= {}
+
+ if (newToolNodeInstance.transformNodeInputsToToolArgs) {
+ const defaultParams = newToolNodeInstance.transformNodeInputsToToolArgs(newNodeData)
+
+ toolCallArgs = {
+ ...defaultParams,
+ ...toolCallArgs
+ }
+ }
+
for (const item of toolInputArgs) {
const variableName = item.inputArgName
const variableValue = item.inputArgValue
@@ -262,6 +278,17 @@ class Tool_Agentflow implements INode {
}
}
+ let toolInput
+ if (typeof toolOutput === 'string' && toolOutput.includes(TOOL_ARGS_PREFIX)) {
+ const [output, args] = toolOutput.split(TOOL_ARGS_PREFIX)
+ toolOutput = output
+ try {
+ toolInput = JSON.parse(args)
+ } catch (e) {
+ console.error('Error parsing tool input from tool:', e)
+ }
+ }
+
if (typeof toolOutput === 'object') {
toolOutput = JSON.stringify(toolOutput, null, 2)
}
@@ -284,7 +311,7 @@ class Tool_Agentflow implements INode {
id: nodeData.id,
name: this.name,
input: {
- toolInputArgs: toolInputArgs,
+ toolInputArgs: toolInput ?? toolInputArgs,
selectedTool: selectedTool
},
output: {
diff --git a/packages/components/nodes/agentflow/prompt.ts b/packages/components/nodes/agentflow/prompt.ts
index a5d9cd89357..ee941ae22fd 100644
--- a/packages/components/nodes/agentflow/prompt.ts
+++ b/packages/components/nodes/agentflow/prompt.ts
@@ -39,37 +39,38 @@ export const DEFAULT_HUMAN_INPUT_DESCRIPTION_HTML = `Summarize the conversati
`
-export const CONDITION_AGENT_SYSTEM_PROMPT = `You are part of a multi-agent system designed to make agent coordination and execution easy. Your task is to analyze the given input and select one matching scenario from a provided set of scenarios. If none of the scenarios match the input, you should return "default."
-
-- **Input**: A string representing the user's query or message.
-- **Scenarios**: A list of predefined scenarios that relate to the input.
-- **Instruction**: Determine if the input fits any of the scenarios.
-
-## Steps
-
-1. **Read the input string** and the list of scenarios.
-2. **Analyze the content of the input** to identify its main topic or intention.
-3. **Compare the input with each scenario**:
- - If a scenario matches the main topic of the input, select that scenario.
- - If no scenarios match, prepare to output "\`\`\`json\n{"output": "default"}\`\`\`"
-4. **Output the result**: If a match is found, return the corresponding scenario in JSON; otherwise, return "\`\`\`json\n{"output": "default"}\`\`\`"
-
-## Output Format
-
-Output should be a JSON object that either names the matching scenario or returns "\`\`\`json\n{"output": "default"}\`\`\`" if no scenarios match. No explanation is needed.
-
-## Examples
-
-1. **Input**: {"input": "Hello", "scenarios": ["user is asking about AI", "default"], "instruction": "Your task is to check and see if user is asking topic about AI"}
- **Output**: "\`\`\`json\n{"output": "default"}\`\`\`"
-
-2. **Input**: {"input": "What is AIGC?", "scenarios": ["user is asking about AI", "default"], "instruction": "Your task is to check and see if user is asking topic about AI"}
- **Output**: "\`\`\`json\n{"output": "user is asking about AI"}\`\`\`"
-
-3. **Input**: {"input": "Can you explain deep learning?", "scenarios": ["user is interested in AI topics", "default"], "instruction": "Determine if the user is interested in learning about AI"}
- **Output**: "\`\`\`json\n{"output": "user is interested in AI topics"}\`\`\`"
-
-## Note
-- Ensure that the input scenarios align well with potential user queries for accurate matching
-- DO NOT include anything other than the JSON in your response.
-`
+export const CONDITION_AGENT_SYSTEM_PROMPT = `
You are part of a multi-agent system designed to make agent coordination and execution easy. Your task is to analyze the given input and select one matching scenario from a provided set of scenarios.
+
+ Input : A string representing the user's query, message or data.
+ Scenarios : A list of predefined scenarios that relate to the input.
+ Instruction : Determine which of the provided scenarios is the best fit for the input.
+
+ Steps
+
+ Read the input string and the list of scenarios.
+ Analyze the content of the input to identify its main topic or intention.
+ Compare the input with each scenario : Evaluate how well the input's topic or intention aligns with each of the provided scenarios and select the one that is the best fit.
+ Output the result : Return the selected scenario in the specified JSON format.
+
+ Output Format
+ Output should be a JSON object that names the selected scenario, like this: {"output": ""} . No explanation is needed.
+ Examples
+
+
+ Input : {"input": "Hello", "scenarios": ["user is asking about AI", "user is not asking about AI"], "instruction": "Your task is to check if the user is asking about AI."}
+ Output : {"output": "user is not asking about AI"}
+
+
+ Input : {"input": "What is AIGC?", "scenarios": ["user is asking about AI", "user is asking about the weather"], "instruction": "Your task is to check and see if the user is asking a topic about AI."}
+ Output : {"output": "user is asking about AI"}
+
+
+ Input : {"input": "Can you explain deep learning?", "scenarios": ["user is interested in AI topics", "user wants to order food"], "instruction": "Determine if the user is interested in learning about AI."}
+ Output : {"output": "user is interested in AI topics"}
+
+
+ Note
+
+ Ensure that the input scenarios align well with potential user queries for accurate matching.
+ DO NOT include anything other than the JSON in your response.
+ `
diff --git a/packages/components/nodes/documentloaders/API/APILoader.ts b/packages/components/nodes/documentloaders/API/APILoader.ts
index 02b77f789ee..5b88e30127f 100644
--- a/packages/components/nodes/documentloaders/API/APILoader.ts
+++ b/packages/components/nodes/documentloaders/API/APILoader.ts
@@ -1,8 +1,10 @@
-import axios, { AxiosRequestConfig } from 'axios'
-import { omit } from 'lodash'
import { Document } from '@langchain/core/documents'
-import { TextSplitter } from 'langchain/text_splitter'
+import axios, { AxiosRequestConfig } from 'axios'
+import * as https from 'https'
import { BaseDocumentLoader } from 'langchain/document_loaders/base'
+import { TextSplitter } from 'langchain/text_splitter'
+import { omit } from 'lodash'
+import { getFileFromStorage } from '../../../src'
import { ICommonObject, IDocument, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface'
import { handleEscapeCharacters } from '../../../src/utils'
@@ -21,7 +23,7 @@ class API_DocumentLoaders implements INode {
constructor() {
this.label = 'API Loader'
this.name = 'apiLoader'
- this.version = 2.0
+ this.version = 2.1
this.type = 'Document'
this.icon = 'api.svg'
this.category = 'Document Loaders'
@@ -61,6 +63,15 @@ class API_DocumentLoaders implements INode {
additionalParams: true,
optional: true
},
+ {
+ label: 'SSL Certificate',
+ description: 'Please upload a SSL certificate file in either .pem or .crt',
+ name: 'caFile',
+ type: 'file',
+ fileType: '.pem, .crt',
+ additionalParams: true,
+ optional: true
+ },
{
label: 'Body',
name: 'body',
@@ -105,8 +116,10 @@ class API_DocumentLoaders implements INode {
}
]
}
- async init(nodeData: INodeData): Promise {
+
+ async init(nodeData: INodeData, _: string, options: ICommonObject): Promise {
const headers = nodeData.inputs?.headers as string
+ const caFileBase64 = nodeData.inputs?.caFile as string
const url = nodeData.inputs?.url as string
const body = nodeData.inputs?.body as string
const method = nodeData.inputs?.method as string
@@ -120,22 +133,37 @@ class API_DocumentLoaders implements INode {
omitMetadataKeys = _omitMetadataKeys.split(',').map((key) => key.trim())
}
- const options: ApiLoaderParams = {
+ const apiLoaderParam: ApiLoaderParams = {
url,
method
}
if (headers) {
const parsedHeaders = typeof headers === 'object' ? headers : JSON.parse(headers)
- options.headers = parsedHeaders
+ apiLoaderParam.headers = parsedHeaders
+ }
+
+ if (caFileBase64.startsWith('FILE-STORAGE::')) {
+ let file = caFileBase64.replace('FILE-STORAGE::', '')
+ file = file.replace('[', '')
+ file = file.replace(']', '')
+ const orgId = options.orgId
+ const chatflowid = options.chatflowid
+ const fileData = await getFileFromStorage(file, orgId, chatflowid)
+ apiLoaderParam.ca = fileData.toString()
+ } else {
+ const splitDataURI = caFileBase64.split(',')
+ splitDataURI.pop()
+ const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
+ apiLoaderParam.ca = bf.toString('utf-8')
}
if (body) {
const parsedBody = typeof body === 'object' ? body : JSON.parse(body)
- options.body = parsedBody
+ apiLoaderParam.body = parsedBody
}
- const loader = new ApiLoader(options)
+ const loader = new ApiLoader(apiLoaderParam)
let docs: IDocument[] = []
@@ -195,6 +223,7 @@ interface ApiLoaderParams {
method: string
headers?: ICommonObject
body?: ICommonObject
+ ca?: string
}
class ApiLoader extends BaseDocumentLoader {
@@ -206,28 +235,36 @@ class ApiLoader extends BaseDocumentLoader {
public readonly method: string
- constructor({ url, headers, body, method }: ApiLoaderParams) {
+ public readonly ca?: string
+
+ constructor({ url, headers, body, method, ca }: ApiLoaderParams) {
super()
this.url = url
this.headers = headers
this.body = body
this.method = method
+ this.ca = ca
}
public async load(): Promise {
if (this.method === 'POST') {
- return this.executePostRequest(this.url, this.headers, this.body)
+ return this.executePostRequest(this.url, this.headers, this.body, this.ca)
} else {
- return this.executeGetRequest(this.url, this.headers)
+ return this.executeGetRequest(this.url, this.headers, this.ca)
}
}
- protected async executeGetRequest(url: string, headers?: ICommonObject): Promise {
+ protected async executeGetRequest(url: string, headers?: ICommonObject, ca?: string): Promise {
try {
const config: AxiosRequestConfig = {}
if (headers) {
config.headers = headers
}
+ if (ca) {
+ config.httpsAgent = new https.Agent({
+ ca: ca
+ })
+ }
const response = await axios.get(url, config)
const responseJsonString = JSON.stringify(response.data, null, 2)
const doc = new Document({
@@ -242,12 +279,17 @@ class ApiLoader extends BaseDocumentLoader {
}
}
- protected async executePostRequest(url: string, headers?: ICommonObject, body?: ICommonObject): Promise {
+ protected async executePostRequest(url: string, headers?: ICommonObject, body?: ICommonObject, ca?: string): Promise {
try {
const config: AxiosRequestConfig = {}
if (headers) {
config.headers = headers
}
+ if (ca) {
+ config.httpsAgent = new https.Agent({
+ ca: ca
+ })
+ }
const response = await axios.post(url, body ?? {}, config)
const responseJsonString = JSON.stringify(response.data, null, 2)
const doc = new Document({
diff --git a/packages/components/nodes/documentloaders/File/File.ts b/packages/components/nodes/documentloaders/File/File.ts
index 345a4ccc571..d3049553f1b 100644
--- a/packages/components/nodes/documentloaders/File/File.ts
+++ b/packages/components/nodes/documentloaders/File/File.ts
@@ -7,6 +7,8 @@ import { CSVLoader } from '@langchain/community/document_loaders/fs/csv'
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'
import { DocxLoader } from '@langchain/community/document_loaders/fs/docx'
import { BaseDocumentLoader } from 'langchain/document_loaders/base'
+import { LoadOfSheet } from '../MicrosoftExcel/ExcelLoader'
+import { PowerpointLoader } from '../MicrosoftPowerpoint/PowerpointLoader'
import { Document } from '@langchain/core/documents'
import { getFileFromStorage } from '../../../src/storageUtils'
import { handleEscapeCharacters, mapMimeTypeToExt } from '../../../src/utils'
@@ -213,10 +215,14 @@ class File_DocumentLoaders implements INode {
jsonl: (blob) => new JSONLinesLoader(blob, '/' + pointerName.trim()),
txt: (blob) => new TextLoader(blob),
csv: (blob) => new CSVLoader(blob),
- xls: (blob) => new CSVLoader(blob),
- xlsx: (blob) => new CSVLoader(blob),
+ xls: (blob) => new LoadOfSheet(blob),
+ xlsx: (blob) => new LoadOfSheet(blob),
+ xlsm: (blob) => new LoadOfSheet(blob),
+ xlsb: (blob) => new LoadOfSheet(blob),
docx: (blob) => new DocxLoader(blob),
doc: (blob) => new DocxLoader(blob),
+ ppt: (blob) => new PowerpointLoader(blob),
+ pptx: (blob) => new PowerpointLoader(blob),
pdf: (blob) =>
pdfUsage === 'perFile'
? // @ts-ignore
diff --git a/packages/components/nodes/documentloaders/FireCrawl/FireCrawl.ts b/packages/components/nodes/documentloaders/FireCrawl/FireCrawl.ts
index 2c1778f422b..27bc3c5b669 100644
--- a/packages/components/nodes/documentloaders/FireCrawl/FireCrawl.ts
+++ b/packages/components/nodes/documentloaders/FireCrawl/FireCrawl.ts
@@ -67,6 +67,29 @@ interface ExtractResponse {
data?: Record
}
+interface SearchResult {
+ url: string
+ title: string
+ description: string
+}
+
+interface SearchResponse {
+ success: boolean
+ data?: SearchResult[]
+ warning?: string
+}
+
+interface SearchRequest {
+ query: string
+ limit?: number
+ tbs?: string
+ lang?: string
+ country?: string
+ location?: string
+ timeout?: number
+ ignoreInvalidURLs?: boolean
+}
+
interface Params {
[key: string]: any
extractorOptions?: {
@@ -161,7 +184,11 @@ class FirecrawlApp {
}
try {
- const response: AxiosResponse = await this.postRequest(this.apiUrl + '/v1/scrape', validParams, headers)
+ const parameters = {
+ ...validParams,
+ integration: 'flowise'
+ }
+ const response: AxiosResponse = await this.postRequest(this.apiUrl + '/v1/scrape', parameters, headers)
if (response.status === 200) {
const responseData = response.data
if (responseData.success) {
@@ -259,7 +286,11 @@ class FirecrawlApp {
}
try {
- const response: AxiosResponse = await this.postRequest(this.apiUrl + '/v1/crawl', validParams, headers)
+ const parameters = {
+ ...validParams,
+ integration: 'flowise'
+ }
+ const response: AxiosResponse = await this.postRequest(this.apiUrl + '/v1/crawl', parameters, headers)
if (response.status === 200) {
const crawlResponse = response.data as CrawlResponse
if (!crawlResponse.success) {
@@ -367,7 +398,11 @@ class FirecrawlApp {
}
try {
- const response: AxiosResponse = await this.postRequest(this.apiUrl + '/v1/extract', validParams, headers)
+ const parameters = {
+ ...validParams,
+ integration: 'flowise'
+ }
+ const response: AxiosResponse = await this.postRequest(this.apiUrl + '/v1/extract', parameters, headers)
if (response.status === 200) {
const extractResponse = response.data as ExtractResponse
if (waitUntilDone) {
@@ -384,18 +419,55 @@ class FirecrawlApp {
return { success: false, id: '', url: '' }
}
+ async search(request: SearchRequest): Promise {
+ const headers = this.prepareHeaders()
+
+ // Create a clean payload with only valid parameters
+ const validParams: any = {
+ query: request.query
+ }
+
+ // Add optional parameters if they exist and are not empty
+ const validSearchParams = ['limit', 'tbs', 'lang', 'country', 'location', 'timeout', 'ignoreInvalidURLs'] as const
+
+ validSearchParams.forEach((param) => {
+ if (request[param] !== undefined && request[param] !== null) {
+ validParams[param] = request[param]
+ }
+ })
+
+ try {
+ const parameters = {
+ ...validParams,
+ integration: 'flowise'
+ }
+ const response: AxiosResponse = await this.postRequest(this.apiUrl + '/v1/search', parameters, headers)
+ if (response.status === 200) {
+ const searchResponse = response.data as SearchResponse
+ if (!searchResponse.success) {
+ throw new Error(`Search request failed: ${searchResponse.warning || 'Unknown error'}`)
+ }
+ return searchResponse
+ } else {
+ this.handleError(response, 'perform search')
+ }
+ } catch (error: any) {
+ throw new Error(error.message)
+ }
+ return { success: false }
+ }
+
private prepareHeaders(idempotencyKey?: string): AxiosRequestHeaders {
return {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
- 'X-Origin': 'flowise',
- 'X-Origin-Type': 'integration',
...(idempotencyKey ? { 'x-idempotency-key': idempotencyKey } : {})
- } as AxiosRequestHeaders & { 'X-Origin': string; 'X-Origin-Type': string; 'x-idempotency-key'?: string }
+ } as AxiosRequestHeaders & { 'x-idempotency-key'?: string }
}
- private postRequest(url: string, data: Params, headers: AxiosRequestHeaders): Promise {
- return axios.post(url, data, { headers })
+ private async postRequest(url: string, data: Params, headers: AxiosRequestHeaders): Promise {
+ const result = await axios.post(url, data, { headers })
+ return result
}
private getRequest(url: string, headers: AxiosRequestHeaders): Promise {
@@ -468,29 +540,32 @@ class FirecrawlApp {
// FireCrawl Loader
interface FirecrawlLoaderParameters {
- url: string
+ url?: string
+ query?: string
apiKey?: string
apiUrl?: string
- mode?: 'crawl' | 'scrape' | 'extract'
+ mode?: 'crawl' | 'scrape' | 'extract' | 'search'
params?: Record
}
export class FireCrawlLoader extends BaseDocumentLoader {
private apiKey: string
private apiUrl: string
- private url: string
- private mode: 'crawl' | 'scrape' | 'extract'
+ private url?: string
+ private query?: string
+ private mode: 'crawl' | 'scrape' | 'extract' | 'search'
private params?: Record
constructor(loaderParams: FirecrawlLoaderParameters) {
super()
- const { apiKey, apiUrl, url, mode = 'crawl', params } = loaderParams
+ const { apiKey, apiUrl, url, query, mode = 'crawl', params } = loaderParams
if (!apiKey) {
throw new Error('Firecrawl API key not set. You can set it as FIRECRAWL_API_KEY in your .env file, or pass it to Firecrawl.')
}
this.apiKey = apiKey
this.url = url
+ this.query = query
this.mode = mode
this.params = params
this.apiUrl = apiUrl || 'https://api.firecrawl.dev'
@@ -500,13 +575,37 @@ export class FireCrawlLoader extends BaseDocumentLoader {
const app = new FirecrawlApp({ apiKey: this.apiKey, apiUrl: this.apiUrl })
let firecrawlDocs: FirecrawlDocument[]
- if (this.mode === 'scrape') {
+ if (this.mode === 'search') {
+ if (!this.query) {
+ throw new Error('Firecrawl: Query is required for search mode')
+ }
+ const response = await app.search({ query: this.query, ...this.params })
+ if (!response.success) {
+ throw new Error(`Firecrawl: Failed to search. Warning: ${response.warning}`)
+ }
+
+ // Convert search results to FirecrawlDocument format
+ firecrawlDocs = (response.data || []).map((result) => ({
+ markdown: result.description,
+ metadata: {
+ title: result.title,
+ sourceURL: result.url,
+ description: result.description
+ }
+ }))
+ } else if (this.mode === 'scrape') {
+ if (!this.url) {
+ throw new Error('Firecrawl: URL is required for scrape mode')
+ }
const response = await app.scrapeUrl(this.url, this.params)
if (!response.success) {
throw new Error(`Firecrawl: Failed to scrape URL. Error: ${response.error}`)
}
firecrawlDocs = [response.data as FirecrawlDocument]
} else if (this.mode === 'crawl') {
+ if (!this.url) {
+ throw new Error('Firecrawl: URL is required for crawl mode')
+ }
const response = await app.crawlUrl(this.url, this.params)
if ('status' in response) {
if (response.status === 'failed') {
@@ -520,6 +619,9 @@ export class FireCrawlLoader extends BaseDocumentLoader {
firecrawlDocs = [response.data as FirecrawlDocument]
}
} else if (this.mode === 'extract') {
+ if (!this.url) {
+ throw new Error('Firecrawl: URL is required for extract mode')
+ }
this.params!.urls = [this.url]
const response = await app.extract(this.params as any as ExtractRequest)
if (!response.success) {
@@ -557,7 +659,7 @@ export class FireCrawlLoader extends BaseDocumentLoader {
}
return []
} else {
- throw new Error(`Unrecognized mode '${this.mode}'. Expected one of 'crawl', 'scrape', 'extract'.`)
+ throw new Error(`Unrecognized mode '${this.mode}'. Expected one of 'crawl', 'scrape', 'extract', 'search'.`)
}
// Convert Firecrawl documents to LangChain documents
@@ -602,7 +704,7 @@ class FireCrawl_DocumentLoaders implements INode {
this.name = 'fireCrawl'
this.type = 'Document'
this.icon = 'firecrawl.png'
- this.version = 3.0
+ this.version = 4.0
this.category = 'Document Loaders'
this.description = 'Load data from URL using FireCrawl'
this.baseClasses = [this.type]
@@ -620,14 +722,7 @@ class FireCrawl_DocumentLoaders implements INode {
optional: true
},
{
- label: 'URLs',
- name: 'url',
- type: 'string',
- description: 'URL to be crawled/scraped/extracted',
- placeholder: 'https://docs.flowiseai.com'
- },
- {
- label: 'Crawler type',
+ label: 'Type',
type: 'options',
name: 'crawlerType',
options: [
@@ -645,89 +740,179 @@ class FireCrawl_DocumentLoaders implements INode {
label: 'Extract',
name: 'extract',
description: 'Extract data from a URL'
+ },
+ {
+ label: 'Search',
+ name: 'search',
+ description: 'Search the web using FireCrawl'
}
],
default: 'crawl'
},
+ {
+ label: 'URLs',
+ name: 'url',
+ type: 'string',
+ description: 'URL to be crawled/scraped/extracted',
+ placeholder: 'https://docs.flowiseai.com',
+ optional: true,
+ show: {
+ crawlerType: ['crawl', 'scrape', 'extract']
+ }
+ },
{
// includeTags
- label: '[Scrape] Include Tags',
+ label: 'Include Tags',
name: 'includeTags',
type: 'string',
description: 'Tags to include in the output. Use comma to separate multiple tags.',
optional: true,
- additionalParams: true
+ additionalParams: true,
+ show: {
+ crawlerType: ['scrape']
+ }
},
{
// excludeTags
- label: '[Scrape] Exclude Tags',
+ label: 'Exclude Tags',
name: 'excludeTags',
type: 'string',
description: 'Tags to exclude from the output. Use comma to separate multiple tags.',
optional: true,
- additionalParams: true
+ additionalParams: true,
+ show: {
+ crawlerType: ['scrape']
+ }
},
{
// onlyMainContent
- label: '[Scrape] Only Main Content',
+ label: 'Only Main Content',
name: 'onlyMainContent',
type: 'boolean',
description: 'Extract only the main content of the page',
optional: true,
- additionalParams: true
+ additionalParams: true,
+ show: {
+ crawlerType: ['scrape']
+ }
},
{
// limit
- label: '[Crawl] Limit',
+ label: 'Limit',
name: 'limit',
type: 'string',
description: 'Maximum number of pages to crawl',
optional: true,
additionalParams: true,
- default: '10000'
+ default: '10000',
+ show: {
+ crawlerType: ['crawl']
+ }
},
{
- label: '[Crawl] Include Paths',
+ label: 'Include Paths',
name: 'includePaths',
type: 'string',
description:
'URL pathname regex patterns that include matching URLs in the crawl. Only the paths that match the specified patterns will be included in the response.',
placeholder: `blog/.*, news/.*`,
optional: true,
- additionalParams: true
+ additionalParams: true,
+ show: {
+ crawlerType: ['crawl']
+ }
},
{
- label: '[Crawl] Exclude Paths',
+ label: 'Exclude Paths',
name: 'excludePaths',
type: 'string',
description: 'URL pathname regex patterns that exclude matching URLs from the crawl.',
placeholder: `blog/.*, news/.*`,
optional: true,
- additionalParams: true
+ additionalParams: true,
+ show: {
+ crawlerType: ['crawl']
+ }
},
{
- label: '[Extract] Schema',
+ label: 'Schema',
name: 'extractSchema',
type: 'json',
description: 'JSON schema for data extraction',
optional: true,
- additionalParams: true
+ additionalParams: true,
+ show: {
+ crawlerType: ['extract']
+ }
},
{
- label: '[Extract] Prompt',
+ label: 'Prompt',
name: 'extractPrompt',
type: 'string',
description: 'Prompt for data extraction',
optional: true,
- additionalParams: true
+ additionalParams: true,
+ show: {
+ crawlerType: ['extract']
+ }
},
{
- label: '[Extract] Job ID',
- name: 'extractJobId',
+ label: 'Query',
+ name: 'searchQuery',
type: 'string',
- description: 'ID of the extract job',
+ description: 'Search query to find relevant content',
optional: true,
- additionalParams: true
+ show: {
+ crawlerType: ['search']
+ }
+ },
+ {
+ label: 'Limit',
+ name: 'searchLimit',
+ type: 'string',
+ description: 'Maximum number of results to return',
+ optional: true,
+ additionalParams: true,
+ default: '5',
+ show: {
+ crawlerType: ['search']
+ }
+ },
+ {
+ label: 'Language',
+ name: 'searchLang',
+ type: 'string',
+ description: 'Language code for search results (e.g., en, es, fr)',
+ optional: true,
+ additionalParams: true,
+ default: 'en',
+ show: {
+ crawlerType: ['search']
+ }
+ },
+ {
+ label: 'Country',
+ name: 'searchCountry',
+ type: 'string',
+ description: 'Country code for search results (e.g., us, uk, ca)',
+ optional: true,
+ additionalParams: true,
+ default: 'us',
+ show: {
+ crawlerType: ['search']
+ }
+ },
+ {
+ label: 'Timeout',
+ name: 'searchTimeout',
+ type: 'number',
+ description: 'Timeout in milliseconds for search operation',
+ optional: true,
+ additionalParams: true,
+ default: 60000,
+ show: {
+ crawlerType: ['search']
+ }
}
]
this.outputs = [
@@ -758,6 +943,11 @@ class FireCrawl_DocumentLoaders implements INode {
const firecrawlApiUrl = getCredentialParam('firecrawlApiUrl', credentialData, nodeData, 'https://api.firecrawl.dev')
const output = nodeData.outputs?.output as string
+ // Validate URL only for non-search methods
+ if (crawlerType !== 'search' && !url) {
+ throw new Error('Firecrawl: URL is required for ' + crawlerType + ' mode')
+ }
+
const includePaths = nodeData.inputs?.includePaths ? (nodeData.inputs.includePaths.split(',') as string[]) : undefined
const excludePaths = nodeData.inputs?.excludePaths ? (nodeData.inputs.excludePaths.split(',') as string[]) : undefined
@@ -767,9 +957,16 @@ class FireCrawl_DocumentLoaders implements INode {
const extractSchema = nodeData.inputs?.extractSchema
const extractPrompt = nodeData.inputs?.extractPrompt as string
+ const searchQuery = nodeData.inputs?.searchQuery as string
+ const searchLimit = nodeData.inputs?.searchLimit as string
+ const searchLang = nodeData.inputs?.searchLang as string
+ const searchCountry = nodeData.inputs?.searchCountry as string
+ const searchTimeout = nodeData.inputs?.searchTimeout as number
+
const input: FirecrawlLoaderParameters = {
url,
- mode: crawlerType as 'crawl' | 'scrape' | 'extract',
+ query: searchQuery,
+ mode: crawlerType as 'crawl' | 'scrape' | 'extract' | 'search',
apiKey: firecrawlApiToken,
apiUrl: firecrawlApiUrl,
params: {
@@ -785,6 +982,19 @@ class FireCrawl_DocumentLoaders implements INode {
}
}
+ // Add search-specific parameters only when in search mode
+ if (crawlerType === 'search') {
+ if (!searchQuery) {
+ throw new Error('Firecrawl: Search query is required for search mode')
+ }
+ input.params = {
+ limit: searchLimit ? parseInt(searchLimit, 10) : 5,
+ lang: searchLang,
+ country: searchCountry,
+ timeout: searchTimeout
+ }
+ }
+
if (onlyMainContent === true) {
const scrapeOptions = input.params?.scrapeOptions as any
input.params!.scrapeOptions = {
diff --git a/packages/components/nodes/documentloaders/Folder/Folder.ts b/packages/components/nodes/documentloaders/Folder/Folder.ts
index 1a6afe05718..d567f64aaf7 100644
--- a/packages/components/nodes/documentloaders/Folder/Folder.ts
+++ b/packages/components/nodes/documentloaders/Folder/Folder.ts
@@ -7,6 +7,8 @@ import { JSONLinesLoader, JSONLoader } from 'langchain/document_loaders/fs/json'
import { CSVLoader } from '@langchain/community/document_loaders/fs/csv'
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'
import { DocxLoader } from '@langchain/community/document_loaders/fs/docx'
+import { LoadOfSheet } from '../MicrosoftExcel/ExcelLoader'
+import { PowerpointLoader } from '../MicrosoftPowerpoint/PowerpointLoader'
import { handleEscapeCharacters } from '../../../src/utils'
class Folder_DocumentLoaders implements INode {
@@ -135,10 +137,14 @@ class Folder_DocumentLoaders implements INode {
'.jsonl': (blob) => new JSONLinesLoader(blob, '/' + pointerName.trim()),
'.txt': (path) => new TextLoader(path),
'.csv': (path) => new CSVLoader(path),
- '.xls': (path) => new CSVLoader(path),
- '.xlsx': (path) => new CSVLoader(path),
+ '.xls': (path) => new LoadOfSheet(path),
+ '.xlsx': (path) => new LoadOfSheet(path),
+ '.xlsm': (path) => new LoadOfSheet(path),
+ '.xlsb': (path) => new LoadOfSheet(path),
'.doc': (path) => new DocxLoader(path),
'.docx': (path) => new DocxLoader(path),
+ '.ppt': (path) => new PowerpointLoader(path),
+ '.pptx': (path) => new PowerpointLoader(path),
'.pdf': (path) =>
pdfUsage === 'perFile'
? // @ts-ignore
diff --git a/packages/components/nodes/documentloaders/GoogleDrive/GoogleDrive.ts b/packages/components/nodes/documentloaders/GoogleDrive/GoogleDrive.ts
new file mode 100644
index 00000000000..441eb61a326
--- /dev/null
+++ b/packages/components/nodes/documentloaders/GoogleDrive/GoogleDrive.ts
@@ -0,0 +1,828 @@
+import { omit } from 'lodash'
+import { ICommonObject, IDocument, INode, INodeData, INodeParams, INodeOptionsValue } from '../../../src/Interface'
+import { TextSplitter } from 'langchain/text_splitter'
+import {
+ convertMultiOptionsToStringArray,
+ getCredentialData,
+ getCredentialParam,
+ handleEscapeCharacters,
+ INodeOutputsValue,
+ refreshOAuth2Token
+} from '../../../src'
+import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'
+import { DocxLoader } from '@langchain/community/document_loaders/fs/docx'
+import { CSVLoader } from '@langchain/community/document_loaders/fs/csv'
+import * as fs from 'fs'
+import * as path from 'path'
+import * as os from 'os'
+import { LoadOfSheet } from '../MicrosoftExcel/ExcelLoader'
+import { PowerpointLoader } from '../MicrosoftPowerpoint/PowerpointLoader'
+
+// Helper function to get human-readable MIME type labels
+const getMimeTypeLabel = (mimeType: string): string | undefined => {
+ const mimeTypeLabels: { [key: string]: string } = {
+ 'application/vnd.google-apps.document': 'Google Doc',
+ 'application/vnd.google-apps.spreadsheet': 'Google Sheet',
+ 'application/vnd.google-apps.presentation': 'Google Slides',
+ 'application/pdf': 'PDF',
+ 'text/plain': 'Text File',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'Word Doc',
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'PowerPoint',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'Excel File'
+ }
+ return mimeTypeLabels[mimeType] || undefined
+}
+
+class GoogleDrive_DocumentLoaders implements INode {
+ label: string
+ name: string
+ version: number
+ description: string
+ type: string
+ icon: string
+ category: string
+ baseClasses: string[]
+ credential: INodeParams
+ inputs: INodeParams[]
+ outputs: INodeOutputsValue[]
+
+ constructor() {
+ this.label = 'Google Drive'
+ this.name = 'googleDrive'
+ this.version = 1.0
+ this.type = 'Document'
+ this.icon = 'google-drive.svg'
+ this.category = 'Document Loaders'
+ this.description = `Load documents from Google Drive files`
+ this.baseClasses = [this.type]
+ this.credential = {
+ label: 'Connect Credential',
+ name: 'credential',
+ type: 'credential',
+ description: 'Google Drive OAuth2 Credential',
+ credentialNames: ['googleDriveOAuth2']
+ }
+ this.inputs = [
+ {
+ label: 'Select Files',
+ name: 'selectedFiles',
+ type: 'asyncMultiOptions',
+ loadMethod: 'listFiles',
+ description: 'Select files from your Google Drive',
+ refresh: true
+ },
+ {
+ label: 'Folder ID',
+ name: 'folderId',
+ type: 'string',
+ description: 'Google Drive folder ID to load all files from (alternative to selecting specific files)',
+ placeholder: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms',
+ optional: true
+ },
+ {
+ label: 'File Types',
+ name: 'fileTypes',
+ type: 'multiOptions',
+ description: 'Types of files to load',
+ options: [
+ {
+ label: 'Google Docs',
+ name: 'application/vnd.google-apps.document'
+ },
+ {
+ label: 'Google Sheets',
+ name: 'application/vnd.google-apps.spreadsheet'
+ },
+ {
+ label: 'Google Slides',
+ name: 'application/vnd.google-apps.presentation'
+ },
+ {
+ label: 'PDF Files',
+ name: 'application/pdf'
+ },
+ {
+ label: 'Text Files',
+ name: 'text/plain'
+ },
+ {
+ label: 'Word Documents',
+ name: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
+ },
+ {
+ label: 'PowerPoint',
+ name: 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
+ },
+ {
+ label: 'Excel Files',
+ name: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+ }
+ ],
+ default: [
+ 'application/vnd.google-apps.document',
+ 'application/vnd.google-apps.spreadsheet',
+ 'application/vnd.google-apps.presentation',
+ 'text/plain',
+ 'application/pdf',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+ ],
+ optional: true
+ },
+ {
+ label: 'Include Subfolders',
+ name: 'includeSubfolders',
+ type: 'boolean',
+ description: 'Whether to include files from subfolders when loading from a folder',
+ default: false,
+ optional: true
+ },
+ {
+ label: 'Include Shared Drives',
+ name: 'includeSharedDrives',
+ type: 'boolean',
+ description: 'Whether to include files from shared drives (Team Drives) that you have access to',
+ default: false,
+ optional: true
+ },
+ {
+ label: 'Max Files',
+ name: 'maxFiles',
+ type: 'number',
+ description: 'Maximum number of files to load (default: 50)',
+ default: 50,
+ optional: true
+ },
+ {
+ label: 'Text Splitter',
+ name: 'textSplitter',
+ type: 'TextSplitter',
+ optional: true
+ },
+ {
+ label: 'Additional Metadata',
+ name: 'metadata',
+ type: 'json',
+ description: 'Additional metadata to be added to the extracted documents',
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Omit Metadata Keys',
+ name: 'omitMetadataKeys',
+ type: 'string',
+ rows: 4,
+ description:
+ 'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field',
+ placeholder: 'key1, key2, key3.nestedKey1',
+ optional: true,
+ additionalParams: true
+ }
+ ]
+ this.outputs = [
+ {
+ label: 'Document',
+ name: 'document',
+ description: 'Array of document objects containing metadata and pageContent',
+ baseClasses: [...this.baseClasses, 'json']
+ },
+ {
+ label: 'Text',
+ name: 'text',
+ description: 'Concatenated string from pageContent of documents',
+ baseClasses: ['string', 'json']
+ }
+ ]
+ }
+
+ //@ts-ignore
+ loadMethods = {
+ async listFiles(nodeData: INodeData, options: ICommonObject): Promise {
+ const returnData: INodeOptionsValue[] = []
+
+ try {
+ let credentialData = await getCredentialData(nodeData.credential ?? '', options)
+ credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options)
+ const accessToken = getCredentialParam('access_token', credentialData, nodeData)
+
+ if (!accessToken) {
+ return returnData
+ }
+
+ // Get file types from input to filter
+ const fileTypes = convertMultiOptionsToStringArray(nodeData.inputs?.fileTypes)
+ const includeSharedDrives = nodeData.inputs?.includeSharedDrives as boolean
+ const maxFiles = (nodeData.inputs?.maxFiles as number) || 100
+
+ let query = 'trashed = false'
+
+ // Add file type filter if specified
+ if (fileTypes && fileTypes.length > 0) {
+ const mimeTypeQuery = fileTypes.map((type) => `mimeType='${type}'`).join(' or ')
+ query += ` and (${mimeTypeQuery})`
+ }
+
+ const url = new URL('https://www.googleapis.com/drive/v3/files')
+ url.searchParams.append('q', query)
+ url.searchParams.append('pageSize', Math.min(maxFiles, 1000).toString())
+ url.searchParams.append('fields', 'files(id, name, mimeType, size, createdTime, modifiedTime, webViewLink, driveId)')
+ url.searchParams.append('orderBy', 'modifiedTime desc')
+
+ // Add shared drives support if requested
+ if (includeSharedDrives) {
+ url.searchParams.append('supportsAllDrives', 'true')
+ url.searchParams.append('includeItemsFromAllDrives', 'true')
+ }
+
+ const response = await fetch(url.toString(), {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json'
+ }
+ })
+
+ if (!response.ok) {
+ console.error(`Failed to list files: ${response.statusText}`)
+ return returnData
+ }
+
+ const data = await response.json()
+
+ for (const file of data.files) {
+ const mimeTypeLabel = getMimeTypeLabel(file.mimeType)
+ if (!mimeTypeLabel) {
+ continue
+ }
+
+ // Add drive context to description
+ const driveContext = file.driveId ? ' (Shared Drive)' : ' (My Drive)'
+
+ const obj: INodeOptionsValue = {
+ name: file.id,
+ label: file.name,
+ description: `Type: ${mimeTypeLabel}${driveContext} | Modified: ${new Date(file.modifiedTime).toLocaleDateString()}`
+ }
+ returnData.push(obj)
+ }
+ } catch (error) {
+ console.error('Error listing Google Drive files:', error)
+ }
+
+ return returnData
+ }
+ }
+
+ async init(nodeData: INodeData, _: string, options: ICommonObject): Promise {
+ const selectedFiles = nodeData.inputs?.selectedFiles as string
+ const folderId = nodeData.inputs?.folderId as string
+ const fileTypes = nodeData.inputs?.fileTypes as string[]
+ const includeSubfolders = nodeData.inputs?.includeSubfolders as boolean
+ const includeSharedDrives = nodeData.inputs?.includeSharedDrives as boolean
+ const maxFiles = (nodeData.inputs?.maxFiles as number) || 50
+ const textSplitter = nodeData.inputs?.textSplitter as TextSplitter
+ const metadata = nodeData.inputs?.metadata
+ const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string
+ const output = nodeData.outputs?.output as string
+
+ let omitMetadataKeys: string[] = []
+ if (_omitMetadataKeys) {
+ omitMetadataKeys = _omitMetadataKeys.split(',').map((key) => key.trim())
+ }
+
+ if (!selectedFiles && !folderId) {
+ throw new Error('Either selected files or Folder ID is required')
+ }
+
+ let credentialData = await getCredentialData(nodeData.credential ?? '', options)
+ credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options)
+ const accessToken = getCredentialParam('access_token', credentialData, nodeData)
+
+ if (!accessToken) {
+ throw new Error('No access token found in credential')
+ }
+
+ let docs: IDocument[] = []
+
+ try {
+ let filesToProcess: any[] = []
+
+ if (selectedFiles) {
+ // Load selected files (selectedFiles can be a single ID or comma-separated IDs)
+ let ids: string[] = []
+ if (typeof selectedFiles === 'string' && selectedFiles.startsWith('[') && selectedFiles.endsWith(']')) {
+ ids = convertMultiOptionsToStringArray(selectedFiles)
+ } else if (typeof selectedFiles === 'string') {
+ ids = [selectedFiles]
+ } else if (Array.isArray(selectedFiles)) {
+ ids = selectedFiles
+ }
+ for (const id of ids) {
+ const fileInfo = await this.getFileInfo(id, accessToken, includeSharedDrives)
+ if (fileInfo && this.shouldProcessFile(fileInfo, fileTypes)) {
+ filesToProcess.push(fileInfo)
+ }
+ }
+ } else if (folderId) {
+ // Load files from folder
+ filesToProcess = await this.getFilesFromFolder(
+ folderId,
+ accessToken,
+ fileTypes,
+ includeSubfolders,
+ includeSharedDrives,
+ maxFiles
+ )
+ }
+
+ // Process each file
+ for (const fileInfo of filesToProcess) {
+ try {
+ const doc = await this.processFile(fileInfo, accessToken)
+ if (doc.length > 0) {
+ docs.push(...doc)
+ }
+ } catch (error) {
+ console.warn(`Failed to process file ${fileInfo.name}: ${error.message}`)
+ }
+ }
+
+ // Apply text splitter if provided
+ if (textSplitter && docs.length > 0) {
+ docs = await textSplitter.splitDocuments(docs)
+ }
+
+ // Apply metadata transformations
+ if (metadata) {
+ const parsedMetadata = typeof metadata === 'object' ? metadata : JSON.parse(metadata)
+ docs = docs.map((doc) => ({
+ ...doc,
+ metadata:
+ _omitMetadataKeys === '*'
+ ? {
+ ...parsedMetadata
+ }
+ : omit(
+ {
+ ...doc.metadata,
+ ...parsedMetadata
+ },
+ omitMetadataKeys
+ )
+ }))
+ } else {
+ docs = docs.map((doc) => ({
+ ...doc,
+ metadata:
+ _omitMetadataKeys === '*'
+ ? {}
+ : omit(
+ {
+ ...doc.metadata
+ },
+ omitMetadataKeys
+ )
+ }))
+ }
+ } catch (error) {
+ throw new Error(`Failed to load Google Drive documents: ${error.message}`)
+ }
+
+ if (output === 'document') {
+ return docs
+ } else {
+ let finaltext = ''
+ for (const doc of docs) {
+ finaltext += `${doc.pageContent}\n`
+ }
+ return handleEscapeCharacters(finaltext, false)
+ }
+ }
+
+ private async getFileInfo(fileId: string, accessToken: string, includeSharedDrives: boolean): Promise {
+ const url = new URL(`https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}`)
+ url.searchParams.append('fields', 'id, name, mimeType, size, createdTime, modifiedTime, parents, webViewLink, driveId')
+
+ // Add shared drives support if requested
+ if (includeSharedDrives) {
+ url.searchParams.append('supportsAllDrives', 'true')
+ }
+
+ const response = await fetch(url.toString(), {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json'
+ }
+ })
+
+ if (!response.ok) {
+ throw new Error(`Failed to get file info: ${response.statusText}`)
+ }
+
+ const fileInfo = await response.json()
+
+ // Add drive context to description
+ const driveContext = fileInfo.driveId ? ' (Shared Drive)' : ' (My Drive)'
+
+ return {
+ ...fileInfo,
+ driveContext
+ }
+ }
+
+ private async getFilesFromFolder(
+ folderId: string,
+ accessToken: string,
+ fileTypes: string[] | undefined,
+ includeSubfolders: boolean,
+ includeSharedDrives: boolean,
+ maxFiles: number
+ ): Promise {
+ const files: any[] = []
+ let nextPageToken: string | undefined
+
+ do {
+ let query = `'${folderId}' in parents and trashed = false`
+
+ // Add file type filter if specified
+ if (fileTypes && fileTypes.length > 0) {
+ const mimeTypeQuery = fileTypes.map((type) => `mimeType='${type}'`).join(' or ')
+ query += ` and (${mimeTypeQuery})`
+ }
+
+ const url = new URL('https://www.googleapis.com/drive/v3/files')
+ url.searchParams.append('q', query)
+ url.searchParams.append('pageSize', Math.min(maxFiles - files.length, 1000).toString())
+ url.searchParams.append(
+ 'fields',
+ 'nextPageToken, files(id, name, mimeType, size, createdTime, modifiedTime, parents, webViewLink, driveId)'
+ )
+
+ // Add shared drives support if requested
+ if (includeSharedDrives) {
+ url.searchParams.append('supportsAllDrives', 'true')
+ url.searchParams.append('includeItemsFromAllDrives', 'true')
+ }
+
+ if (nextPageToken) {
+ url.searchParams.append('pageToken', nextPageToken)
+ }
+
+ const response = await fetch(url.toString(), {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json'
+ }
+ })
+
+ if (!response.ok) {
+ throw new Error(`Failed to list files: ${response.statusText}`)
+ }
+
+ const data = await response.json()
+
+ // Add drive context to each file
+ const filesWithContext = data.files.map((file: any) => ({
+ ...file,
+ driveContext: file.driveId ? ' (Shared Drive)' : ' (My Drive)'
+ }))
+
+ files.push(...filesWithContext)
+ nextPageToken = data.nextPageToken
+
+ // If includeSubfolders is true, also get files from subfolders
+ if (includeSubfolders) {
+ for (const file of data.files) {
+ if (file.mimeType === 'application/vnd.google-apps.folder') {
+ const subfolderFiles = await this.getFilesFromFolder(
+ file.id,
+ accessToken,
+ fileTypes,
+ includeSubfolders,
+ includeSharedDrives,
+ maxFiles - files.length
+ )
+ files.push(...subfolderFiles)
+ }
+ }
+ }
+ } while (nextPageToken && files.length < maxFiles)
+
+ return files.slice(0, maxFiles)
+ }
+
+ private shouldProcessFile(fileInfo: any, fileTypes: string[] | undefined): boolean {
+ if (!fileTypes || fileTypes.length === 0) {
+ return true
+ }
+ return fileTypes.includes(fileInfo.mimeType)
+ }
+
+ private async processFile(fileInfo: any, accessToken: string): Promise {
+ let content = ''
+
+ try {
+ // Handle different file types
+ if (this.isTextBasedFile(fileInfo.mimeType)) {
+ // Download regular text files
+ content = await this.downloadFile(fileInfo.id, accessToken)
+
+ // Create document with metadata
+ return [
+ {
+ pageContent: content,
+ metadata: {
+ source: fileInfo.webViewLink || `https://drive.google.com/file/d/${fileInfo.id}/view`,
+ fileId: fileInfo.id,
+ fileName: fileInfo.name,
+ mimeType: fileInfo.mimeType,
+ size: fileInfo.size ? parseInt(fileInfo.size) : undefined,
+ createdTime: fileInfo.createdTime,
+ modifiedTime: fileInfo.modifiedTime,
+ parents: fileInfo.parents,
+ driveId: fileInfo.driveId,
+ driveContext: fileInfo.driveContext || (fileInfo.driveId ? ' (Shared Drive)' : ' (My Drive)')
+ }
+ }
+ ]
+ } else if (this.isSupportedBinaryFile(fileInfo.mimeType) || this.isGoogleWorkspaceFile(fileInfo.mimeType)) {
+ // Process binary files and Google Workspace files using loaders
+ return await this.processBinaryFile(fileInfo, accessToken)
+ } else {
+ console.warn(`Unsupported file type ${fileInfo.mimeType} for file ${fileInfo.name}`)
+ return []
+ }
+ } catch (error) {
+ console.warn(`Failed to process file ${fileInfo.name}: ${error.message}`)
+ return []
+ }
+ }
+
+ private isSupportedBinaryFile(mimeType: string): boolean {
+ const supportedBinaryTypes = [
+ 'application/pdf',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'application/msword',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'application/vnd.ms-excel',
+ 'text/csv'
+ ]
+ return supportedBinaryTypes.includes(mimeType)
+ }
+
+ private async processBinaryFile(fileInfo: any, accessToken: string): Promise {
+ let tempFilePath: string | null = null
+
+ try {
+ let buffer: Buffer
+ let processedMimeType: string
+ let processedFileName: string
+
+ if (this.isGoogleWorkspaceFile(fileInfo.mimeType)) {
+ // Handle Google Workspace files by exporting to appropriate format
+ const exportResult = await this.exportGoogleWorkspaceFileAsBuffer(fileInfo.id, fileInfo.mimeType, accessToken)
+ buffer = exportResult.buffer
+ processedMimeType = exportResult.mimeType
+ processedFileName = exportResult.fileName
+ } else {
+ // Handle regular binary files
+ buffer = await this.downloadBinaryFile(fileInfo.id, accessToken)
+ processedMimeType = fileInfo.mimeType
+ processedFileName = fileInfo.name
+ }
+
+ // Download file to temporary location
+ tempFilePath = await this.createTempFile(buffer, processedFileName, processedMimeType)
+
+ let docs: IDocument[] = []
+ const mimeType = processedMimeType.toLowerCase()
+ switch (mimeType) {
+ case 'application/pdf': {
+ const pdfLoader = new PDFLoader(tempFilePath, {
+ // @ts-ignore
+ pdfjs: () => import('pdf-parse/lib/pdf.js/v1.10.100/build/pdf.js')
+ })
+ docs = await pdfLoader.load()
+ break
+ }
+
+ case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
+ case 'application/msword': {
+ const docxLoader = new DocxLoader(tempFilePath)
+ docs = await docxLoader.load()
+ break
+ }
+
+ case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
+ case 'application/vnd.ms-excel': {
+ const excelLoader = new LoadOfSheet(tempFilePath)
+ docs = await excelLoader.load()
+ break
+ }
+ case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
+ case 'application/vnd.ms-powerpoint': {
+ const pptxLoader = new PowerpointLoader(tempFilePath)
+ docs = await pptxLoader.load()
+ break
+ }
+ case 'text/csv': {
+ const csvLoader = new CSVLoader(tempFilePath)
+ docs = await csvLoader.load()
+ break
+ }
+
+ default:
+ throw new Error(`Unsupported binary file type: ${mimeType}`)
+ }
+
+ // Add Google Drive metadata to each document
+ if (docs.length > 0) {
+ const googleDriveMetadata = {
+ source: fileInfo.webViewLink || `https://drive.google.com/file/d/${fileInfo.id}/view`,
+ fileId: fileInfo.id,
+ fileName: fileInfo.name,
+ mimeType: fileInfo.mimeType,
+ size: fileInfo.size ? parseInt(fileInfo.size) : undefined,
+ createdTime: fileInfo.createdTime,
+ modifiedTime: fileInfo.modifiedTime,
+ parents: fileInfo.parents,
+ totalPages: docs.length // Total number of pages/sheets in the file
+ }
+
+ return docs.map((doc, index) => ({
+ ...doc,
+ metadata: {
+ ...doc.metadata, // Keep original loader metadata (page numbers, etc.)
+ ...googleDriveMetadata, // Add Google Drive metadata
+ pageIndex: index, // Add page/sheet index
+ driveId: fileInfo.driveId,
+ driveContext: fileInfo.driveContext || (fileInfo.driveId ? ' (Shared Drive)' : ' (My Drive)')
+ }
+ }))
+ }
+
+ return []
+ } catch (error) {
+ throw new Error(`Failed to process binary file: ${error.message}`)
+ } finally {
+ // Clean up temporary file
+ if (tempFilePath && fs.existsSync(tempFilePath)) {
+ try {
+ fs.unlinkSync(tempFilePath)
+ } catch (e) {
+ console.warn(`Failed to delete temporary file: ${tempFilePath}`)
+ }
+ }
+ }
+ }
+
+ private async createTempFile(buffer: Buffer, fileName: string, mimeType: string): Promise {
+ // Get appropriate file extension
+ let extension = path.extname(fileName)
+ if (!extension) {
+ const extensionMap: { [key: string]: string } = {
+ 'application/pdf': '.pdf',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
+ 'application/msword': '.doc',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
+ 'application/vnd.ms-excel': '.xls',
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
+ 'application/vnd.ms-powerpoint': '.ppt',
+ 'text/csv': '.csv'
+ }
+ extension = extensionMap[mimeType] || '.tmp'
+ }
+
+ // Create temporary file
+ const tempDir = os.tmpdir()
+ const tempFileName = `gdrive_${Date.now()}_${Math.random().toString(36).substring(7)}${extension}`
+ const tempFilePath = path.join(tempDir, tempFileName)
+
+ fs.writeFileSync(tempFilePath, buffer)
+ return tempFilePath
+ }
+
+ private async downloadBinaryFile(fileId: string, accessToken: string): Promise {
+ const url = `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}?alt=media`
+
+ const response = await fetch(url, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`
+ }
+ })
+
+ if (!response.ok) {
+ throw new Error(`Failed to download file: ${response.statusText}`)
+ }
+
+ const arrayBuffer = await response.arrayBuffer()
+ return Buffer.from(arrayBuffer)
+ }
+
+ private async downloadFile(fileId: string, accessToken: string): Promise {
+ const url = `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}?alt=media`
+
+ const response = await fetch(url, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`
+ }
+ })
+
+ if (!response.ok) {
+ throw new Error(`Failed to download file: ${response.statusText}`)
+ }
+
+ // Only call response.text() for text-based files
+ const contentType = response.headers.get('content-type') || ''
+ if (!contentType.startsWith('text/') && !contentType.includes('json') && !contentType.includes('xml')) {
+ throw new Error(`Cannot process binary file with content-type: ${contentType}`)
+ }
+
+ return await response.text()
+ }
+
+ private isGoogleWorkspaceFile(mimeType: string): boolean {
+ const googleWorkspaceMimeTypes = [
+ 'application/vnd.google-apps.document',
+ 'application/vnd.google-apps.spreadsheet',
+ 'application/vnd.google-apps.presentation',
+ 'application/vnd.google-apps.drawing'
+ ]
+ return googleWorkspaceMimeTypes.includes(mimeType)
+ }
+
+ private isTextBasedFile(mimeType: string): boolean {
+ const textBasedMimeTypes = [
+ 'text/plain',
+ 'text/html',
+ 'text/css',
+ 'text/javascript',
+ 'text/csv',
+ 'text/xml',
+ 'application/json',
+ 'application/xml',
+ 'text/markdown',
+ 'text/x-markdown'
+ ]
+ return textBasedMimeTypes.includes(mimeType)
+ }
+
+ private async exportGoogleWorkspaceFileAsBuffer(
+ fileId: string,
+ mimeType: string,
+ accessToken: string
+ ): Promise<{ buffer: Buffer; mimeType: string; fileName: string }> {
+ // Automatic mapping of Google Workspace MIME types to export formats
+ let exportMimeType: string
+ let fileExtension: string
+
+ switch (mimeType) {
+ case 'application/vnd.google-apps.document':
+ exportMimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
+ fileExtension = '.docx'
+ break
+ case 'application/vnd.google-apps.spreadsheet':
+ exportMimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+ fileExtension = '.xlsx'
+ break
+ case 'application/vnd.google-apps.presentation':
+ exportMimeType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
+ fileExtension = '.pptx'
+ break
+ case 'application/vnd.google-apps.drawing':
+ exportMimeType = 'application/pdf'
+ fileExtension = '.pdf'
+ break
+ default:
+ // Fallback to DOCX for any other Google Workspace file
+ exportMimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
+ fileExtension = '.docx'
+ break
+ }
+
+ const url = `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}/export?mimeType=${encodeURIComponent(
+ exportMimeType
+ )}`
+
+ const response = await fetch(url, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`
+ }
+ })
+
+ if (!response.ok) {
+ throw new Error(`Failed to export file: ${response.statusText}`)
+ }
+
+ const arrayBuffer = await response.arrayBuffer()
+ const buffer = Buffer.from(arrayBuffer)
+
+ return {
+ buffer,
+ mimeType: exportMimeType,
+ fileName: `exported_file${fileExtension}`
+ }
+ }
+}
+
+module.exports = { nodeClass: GoogleDrive_DocumentLoaders }
diff --git a/packages/components/nodes/documentloaders/GoogleDrive/google-drive.svg b/packages/components/nodes/documentloaders/GoogleDrive/google-drive.svg
new file mode 100644
index 00000000000..03b2f21290a
--- /dev/null
+++ b/packages/components/nodes/documentloaders/GoogleDrive/google-drive.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/components/nodes/documentloaders/GoogleSheets/GoogleSheets.ts b/packages/components/nodes/documentloaders/GoogleSheets/GoogleSheets.ts
new file mode 100644
index 00000000000..8c7a7a2a493
--- /dev/null
+++ b/packages/components/nodes/documentloaders/GoogleSheets/GoogleSheets.ts
@@ -0,0 +1,429 @@
+import { omit } from 'lodash'
+import { ICommonObject, IDocument, INode, INodeData, INodeParams, INodeOptionsValue } from '../../../src/Interface'
+import { TextSplitter } from 'langchain/text_splitter'
+import {
+ convertMultiOptionsToStringArray,
+ getCredentialData,
+ getCredentialParam,
+ handleEscapeCharacters,
+ INodeOutputsValue,
+ refreshOAuth2Token
+} from '../../../src'
+
+class GoogleSheets_DocumentLoaders implements INode {
+ label: string
+ name: string
+ version: number
+ description: string
+ type: string
+ icon: string
+ category: string
+ baseClasses: string[]
+ credential: INodeParams
+ inputs: INodeParams[]
+ outputs: INodeOutputsValue[]
+
+ constructor() {
+ this.label = 'Google Sheets'
+ this.name = 'googleSheets'
+ this.version = 1.0
+ this.type = 'Document'
+ this.icon = 'google-sheets.svg'
+ this.category = 'Document Loaders'
+ this.description = `Load data from Google Sheets as documents`
+ this.baseClasses = [this.type]
+ this.credential = {
+ label: 'Connect Credential',
+ name: 'credential',
+ type: 'credential',
+ description: 'Google Sheets OAuth2 Credential',
+ credentialNames: ['googleSheetsOAuth2']
+ }
+ this.inputs = [
+ {
+ label: 'Select Spreadsheet',
+ name: 'spreadsheetIds',
+ type: 'asyncMultiOptions',
+ loadMethod: 'listSpreadsheets',
+ description: 'Select spreadsheet from your Google Drive',
+ refresh: true
+ },
+ {
+ label: 'Sheet Names',
+ name: 'sheetNames',
+ type: 'string',
+ description: 'Comma-separated list of sheet names to load. If empty, loads all sheets.',
+ placeholder: 'Sheet1, Sheet2',
+ optional: true
+ },
+ {
+ label: 'Range',
+ name: 'range',
+ type: 'string',
+ description: 'Range to load (e.g., A1:E10). If empty, loads entire sheet.',
+ placeholder: 'A1:E10',
+ optional: true
+ },
+ {
+ label: 'Include Headers',
+ name: 'includeHeaders',
+ type: 'boolean',
+ description: 'Whether to include the first row as headers',
+ default: true
+ },
+ {
+ label: 'Value Render Option',
+ name: 'valueRenderOption',
+ type: 'options',
+ description: 'How values should be represented in the output',
+ options: [
+ {
+ label: 'Formatted Value',
+ name: 'FORMATTED_VALUE'
+ },
+ {
+ label: 'Unformatted Value',
+ name: 'UNFORMATTED_VALUE'
+ },
+ {
+ label: 'Formula',
+ name: 'FORMULA'
+ }
+ ],
+ default: 'FORMATTED_VALUE',
+ optional: true
+ },
+ {
+ label: 'Text Splitter',
+ name: 'textSplitter',
+ type: 'TextSplitter',
+ optional: true
+ },
+ {
+ label: 'Additional Metadata',
+ name: 'metadata',
+ type: 'json',
+ description: 'Additional metadata to be added to the extracted documents',
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Omit Metadata Keys',
+ name: 'omitMetadataKeys',
+ type: 'string',
+ rows: 4,
+ description:
+ 'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field',
+ placeholder: 'key1, key2, key3.nestedKey1',
+ optional: true,
+ additionalParams: true
+ }
+ ]
+ this.outputs = [
+ {
+ label: 'Document',
+ name: 'document',
+ description: 'Array of document objects containing metadata and pageContent',
+ baseClasses: [...this.baseClasses, 'json']
+ },
+ {
+ label: 'Text',
+ name: 'text',
+ description: 'Concatenated string from pageContent of documents',
+ baseClasses: ['string', 'json']
+ }
+ ]
+ }
+
+ //@ts-ignore
+ loadMethods = {
+ async listSpreadsheets(nodeData: INodeData, options: ICommonObject): Promise {
+ const returnData: INodeOptionsValue[] = []
+
+ try {
+ let credentialData = await getCredentialData(nodeData.credential ?? '', options)
+ credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options)
+ const accessToken = getCredentialParam('access_token', credentialData, nodeData)
+
+ if (!accessToken) {
+ return returnData
+ }
+
+ // Query for Google Sheets files specifically
+ const query = "mimeType='application/vnd.google-apps.spreadsheet' and trashed = false"
+
+ const url = new URL('https://www.googleapis.com/drive/v3/files')
+ url.searchParams.append('q', query)
+ url.searchParams.append('pageSize', '100')
+ url.searchParams.append('fields', 'files(id, name, modifiedTime, webViewLink)')
+ url.searchParams.append('orderBy', 'modifiedTime desc')
+
+ const response = await fetch(url.toString(), {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json'
+ }
+ })
+
+ if (!response.ok) {
+ console.error(`Failed to list spreadsheets: ${response.statusText}`)
+ return returnData
+ }
+
+ const data = await response.json()
+
+ for (const file of data.files) {
+ const obj: INodeOptionsValue = {
+ name: file.id,
+ label: file.name,
+ description: `Modified: ${new Date(file.modifiedTime).toLocaleDateString()}`
+ }
+ returnData.push(obj)
+ }
+ } catch (error) {
+ console.error('Error listing Google Sheets:', error)
+ }
+
+ return returnData
+ }
+ }
+
+ async init(nodeData: INodeData, _: string, options: ICommonObject): Promise {
+ const _spreadsheetIds = nodeData.inputs?.spreadsheetIds as string
+ const sheetNames = nodeData.inputs?.sheetNames as string
+ const range = nodeData.inputs?.range as string
+ const includeHeaders = nodeData.inputs?.includeHeaders as boolean
+ const valueRenderOption = (nodeData.inputs?.valueRenderOption as string) || 'FORMATTED_VALUE'
+ const textSplitter = nodeData.inputs?.textSplitter as TextSplitter
+ const metadata = nodeData.inputs?.metadata
+ const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string
+ const output = nodeData.outputs?.output as string
+
+ let omitMetadataKeys: string[] = []
+ if (_omitMetadataKeys) {
+ omitMetadataKeys = _omitMetadataKeys.split(',').map((key) => key.trim())
+ }
+
+ if (!_spreadsheetIds) {
+ throw new Error('At least one spreadsheet is required')
+ }
+
+ let spreadsheetIds = convertMultiOptionsToStringArray(_spreadsheetIds)
+
+ let credentialData = await getCredentialData(nodeData.credential ?? '', options)
+ credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options)
+ const accessToken = getCredentialParam('access_token', credentialData, nodeData)
+
+ if (!accessToken) {
+ throw new Error('No access token found in credential')
+ }
+
+ let docs: IDocument[] = []
+
+ try {
+ // Process each spreadsheet
+ for (const spreadsheetId of spreadsheetIds) {
+ try {
+ // Get spreadsheet metadata first
+ const spreadsheetMetadata = await this.getSpreadsheetMetadata(spreadsheetId, accessToken)
+
+ // Determine which sheets to load
+ let sheetsToLoad: string[] = []
+ if (sheetNames) {
+ sheetsToLoad = sheetNames.split(',').map((name) => name.trim())
+ } else {
+ // Get all sheet names from metadata
+ sheetsToLoad = spreadsheetMetadata.sheets?.map((sheet: any) => sheet.properties.title) || []
+ }
+
+ // Load data from each sheet
+ for (const sheetName of sheetsToLoad) {
+ const sheetRange = range ? `${sheetName}!${range}` : sheetName
+ const sheetData = await this.getSheetData(spreadsheetId, sheetRange, valueRenderOption, accessToken)
+
+ if (sheetData.values && sheetData.values.length > 0) {
+ const sheetDoc = this.convertSheetToDocument(
+ sheetData,
+ sheetName,
+ spreadsheetId,
+ spreadsheetMetadata,
+ includeHeaders
+ )
+ docs.push(sheetDoc)
+ }
+ }
+ } catch (error) {
+ console.warn(`Failed to process spreadsheet ${spreadsheetId}: ${error.message}`)
+ // Continue processing other spreadsheets even if one fails
+ }
+ }
+
+ // Apply text splitter if provided
+ if (textSplitter && docs.length > 0) {
+ docs = await textSplitter.splitDocuments(docs)
+ }
+
+ // Apply metadata transformations
+ if (metadata) {
+ const parsedMetadata = typeof metadata === 'object' ? metadata : JSON.parse(metadata)
+ docs = docs.map((doc) => ({
+ ...doc,
+ metadata:
+ _omitMetadataKeys === '*'
+ ? {
+ ...parsedMetadata
+ }
+ : omit(
+ {
+ ...doc.metadata,
+ ...parsedMetadata
+ },
+ omitMetadataKeys
+ )
+ }))
+ } else {
+ docs = docs.map((doc) => ({
+ ...doc,
+ metadata:
+ _omitMetadataKeys === '*'
+ ? {}
+ : omit(
+ {
+ ...doc.metadata
+ },
+ omitMetadataKeys
+ )
+ }))
+ }
+ } catch (error) {
+ throw new Error(`Failed to load Google Sheets data: ${error.message}`)
+ }
+
+ if (output === 'document') {
+ return docs
+ } else {
+ let finaltext = ''
+ for (const doc of docs) {
+ finaltext += `${doc.pageContent}\n`
+ }
+ return handleEscapeCharacters(finaltext, false)
+ }
+ }
+
+ private async getSpreadsheetMetadata(spreadsheetId: string, accessToken: string): Promise {
+ const url = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}`
+
+ const response = await fetch(url, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json'
+ }
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ throw new Error(`Failed to get spreadsheet metadata: ${response.status} ${response.statusText} - ${errorText}`)
+ }
+
+ return response.json()
+ }
+
+ private async getSheetData(spreadsheetId: string, range: string, valueRenderOption: string, accessToken: string): Promise {
+ const url = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(range)}`
+ const params = new URLSearchParams({
+ valueRenderOption,
+ dateTimeRenderOption: 'FORMATTED_STRING',
+ majorDimension: 'ROWS'
+ })
+
+ const response = await fetch(`${url}?${params}`, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json'
+ }
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ throw new Error(`Failed to get sheet data: ${response.status} ${response.statusText} - ${errorText}`)
+ }
+
+ return response.json()
+ }
+
+ private convertSheetToDocument(
+ sheetData: any,
+ sheetName: string,
+ spreadsheetId: string,
+ spreadsheetMetadata: any,
+ includeHeaders: boolean
+ ): IDocument {
+ const values = sheetData.values || []
+
+ if (values.length === 0) {
+ return {
+ pageContent: '',
+ metadata: {
+ source: `Google Sheets: ${spreadsheetMetadata.properties?.title || 'Unknown'} - ${sheetName}`,
+ spreadsheetId,
+ sheetName,
+ spreadsheetTitle: spreadsheetMetadata.properties?.title,
+ range: sheetData.range,
+ rowCount: 0,
+ columnCount: 0
+ }
+ }
+ }
+
+ let headers: string[] = []
+ let dataRows: string[][] = []
+
+ if (includeHeaders && values.length > 0) {
+ headers = values[0] || []
+ dataRows = values.slice(1)
+ } else {
+ // Generate default headers like A, B, C, etc.
+ const maxColumns = Math.max(...values.map((row: any[]) => row.length))
+ headers = Array.from({ length: maxColumns }, (_, i) => String.fromCharCode(65 + i))
+ dataRows = values
+ }
+
+ // Convert to markdown table format
+ let content = ''
+
+ if (headers.length > 0) {
+ // Create header row
+ content += '| ' + headers.join(' | ') + ' |\n'
+ // Create separator row
+ content += '| ' + headers.map(() => '---').join(' | ') + ' |\n'
+
+ // Add data rows
+ for (const row of dataRows) {
+ const paddedRow = [...row]
+ // Pad row to match header length
+ while (paddedRow.length < headers.length) {
+ paddedRow.push('')
+ }
+ content += '| ' + paddedRow.join(' | ') + ' |\n'
+ }
+ }
+
+ return {
+ pageContent: content,
+ metadata: {
+ source: `Google Sheets: ${spreadsheetMetadata.properties?.title || 'Unknown'} - ${sheetName}`,
+ spreadsheetId,
+ sheetName,
+ spreadsheetTitle: spreadsheetMetadata.properties?.title,
+ spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`,
+ range: sheetData.range,
+ rowCount: values.length,
+ columnCount: headers.length,
+ headers: includeHeaders ? headers : undefined,
+ totalDataRows: dataRows.length
+ }
+ }
+ }
+}
+
+module.exports = { nodeClass: GoogleSheets_DocumentLoaders }
diff --git a/packages/components/nodes/documentloaders/GoogleSheets/google-sheets.svg b/packages/components/nodes/documentloaders/GoogleSheets/google-sheets.svg
new file mode 100644
index 00000000000..43af0ccf1fe
--- /dev/null
+++ b/packages/components/nodes/documentloaders/GoogleSheets/google-sheets.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/components/nodes/documentloaders/Jira/jira.svg b/packages/components/nodes/documentloaders/Jira/jira.svg
index 807c5a31147..4ace5cc84a3 100644
--- a/packages/components/nodes/documentloaders/Jira/jira.svg
+++ b/packages/components/nodes/documentloaders/Jira/jira.svg
@@ -1,2 +1 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/packages/components/nodes/documentloaders/MicrosoftExcel/ExcelLoader.ts b/packages/components/nodes/documentloaders/MicrosoftExcel/ExcelLoader.ts
new file mode 100644
index 00000000000..1e4889e1ce4
--- /dev/null
+++ b/packages/components/nodes/documentloaders/MicrosoftExcel/ExcelLoader.ts
@@ -0,0 +1,72 @@
+import { Document } from '@langchain/core/documents'
+import { BufferLoader } from 'langchain/document_loaders/fs/buffer'
+import { read, utils } from 'xlsx'
+
+/**
+ * Document loader that uses SheetJS to load documents.
+ *
+ * Each worksheet is parsed into an array of row objects using the SheetJS
+ * `sheet_to_json` method and projected to a `Document`. Metadata includes
+ * original sheet name, row data, and row index
+ */
+export class LoadOfSheet extends BufferLoader {
+ attributes: { name: string; description: string; type: string }[] = []
+
+ constructor(filePathOrBlob: string | Blob) {
+ super(filePathOrBlob)
+ this.attributes = []
+ }
+
+ /**
+ * Parse document
+ *
+ * NOTE: column labels in multiple sheets are not disambiguated!
+ *
+ * @param raw Raw data Buffer
+ * @param metadata Document metadata
+ * @returns Array of Documents
+ */
+ async parse(raw: Buffer, metadata: Document['metadata']): Promise {
+ const result: Document[] = []
+
+ this.attributes = [
+ { name: 'worksheet', description: 'Sheet or Worksheet Name', type: 'string' },
+ { name: 'rowNum', description: 'Row index', type: 'number' }
+ ]
+
+ const wb = read(raw, { type: 'buffer' })
+ for (let name of wb.SheetNames) {
+ const fields: Record> = {}
+ const ws = wb.Sheets[name]
+ if (!ws) continue
+
+ const aoo = utils.sheet_to_json(ws) as Record[]
+ aoo.forEach((row) => {
+ result.push({
+ pageContent:
+ Object.entries(row)
+ .map((kv) => `- ${kv[0]}: ${kv[1]}`)
+ .join('\n') + '\n',
+ metadata: {
+ worksheet: name,
+ rowNum: row['__rowNum__'],
+ ...metadata,
+ ...row
+ }
+ })
+ Object.entries(row).forEach(([k, v]) => {
+ if (v != null) (fields[k] || (fields[k] = {}))[v instanceof Date ? 'date' : typeof v] = true
+ })
+ })
+ Object.entries(fields).forEach(([k, v]) =>
+ this.attributes.push({
+ name: k,
+ description: k,
+ type: Object.keys(v).join(' or ')
+ })
+ )
+ }
+
+ return result
+ }
+}
diff --git a/packages/components/nodes/documentloaders/MicrosoftExcel/MicrosoftExcel.ts b/packages/components/nodes/documentloaders/MicrosoftExcel/MicrosoftExcel.ts
new file mode 100644
index 00000000000..468d16731b9
--- /dev/null
+++ b/packages/components/nodes/documentloaders/MicrosoftExcel/MicrosoftExcel.ts
@@ -0,0 +1,142 @@
+import { TextSplitter } from 'langchain/text_splitter'
+import { LoadOfSheet } from './ExcelLoader'
+import { getFileFromStorage, handleDocumentLoaderDocuments, handleDocumentLoaderMetadata, handleDocumentLoaderOutput } from '../../../src'
+import { ICommonObject, IDocument, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface'
+
+class MicrosoftExcel_DocumentLoaders implements INode {
+ label: string
+ name: string
+ version: number
+ description: string
+ type: string
+ icon: string
+ category: string
+ baseClasses: string[]
+ inputs: INodeParams[]
+ outputs: INodeOutputsValue[]
+
+ constructor() {
+ this.label = 'Microsoft Excel'
+ this.name = 'microsoftExcel'
+ this.version = 1.0
+ this.type = 'Document'
+ this.icon = 'excel.svg'
+ this.category = 'Document Loaders'
+ this.description = `Load data from Microsoft Excel files`
+ this.baseClasses = [this.type]
+ this.inputs = [
+ {
+ label: 'Excel File',
+ name: 'excelFile',
+ type: 'file',
+ fileType: '.xlsx, .xls, .xlsm, .xlsb'
+ },
+ {
+ label: 'Text Splitter',
+ name: 'textSplitter',
+ type: 'TextSplitter',
+ optional: true
+ },
+ {
+ label: 'Additional Metadata',
+ name: 'metadata',
+ type: 'json',
+ description: 'Additional metadata to be added to the extracted documents',
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Omit Metadata Keys',
+ name: 'omitMetadataKeys',
+ type: 'string',
+ rows: 4,
+ description:
+ 'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field',
+ placeholder: 'key1, key2, key3.nestedKey1',
+ optional: true,
+ additionalParams: true
+ }
+ ]
+ this.outputs = [
+ {
+ label: 'Document',
+ name: 'document',
+ description: 'Array of document objects containing metadata and pageContent',
+ baseClasses: [...this.baseClasses, 'json']
+ },
+ {
+ label: 'Text',
+ name: 'text',
+ description: 'Concatenated string from pageContent of documents',
+ baseClasses: ['string', 'json']
+ }
+ ]
+ }
+
+ getFiles(nodeData: INodeData) {
+ const excelFileBase64 = nodeData.inputs?.excelFile as string
+
+ let files: string[] = []
+ let fromStorage: boolean = true
+
+ if (excelFileBase64.startsWith('FILE-STORAGE::')) {
+ const fileName = excelFileBase64.replace('FILE-STORAGE::', '')
+ if (fileName.startsWith('[') && fileName.endsWith(']')) {
+ files = JSON.parse(fileName)
+ } else {
+ files = [fileName]
+ }
+ } else {
+ if (excelFileBase64.startsWith('[') && excelFileBase64.endsWith(']')) {
+ files = JSON.parse(excelFileBase64)
+ } else {
+ files = [excelFileBase64]
+ }
+
+ fromStorage = false
+ }
+
+ return { files, fromStorage }
+ }
+
+ async getFileData(file: string, { orgId, chatflowid }: { orgId: string; chatflowid: string }, fromStorage?: boolean) {
+ if (fromStorage) {
+ return getFileFromStorage(file, orgId, chatflowid)
+ } else {
+ const splitDataURI = file.split(',')
+ splitDataURI.pop()
+ return Buffer.from(splitDataURI.pop() || '', 'base64')
+ }
+ }
+
+ async init(nodeData: INodeData, _: string, options: ICommonObject): Promise {
+ const textSplitter = nodeData.inputs?.textSplitter as TextSplitter
+ const metadata = nodeData.inputs?.metadata
+ const output = nodeData.outputs?.output as string
+ const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string
+
+ let docs: IDocument[] = []
+
+ const orgId = options.orgId
+ const chatflowid = options.chatflowid
+
+ const { files, fromStorage } = this.getFiles(nodeData)
+
+ for (const file of files) {
+ if (!file) continue
+
+ const fileData = await this.getFileData(file, { orgId, chatflowid }, fromStorage)
+ const blob = new Blob([fileData])
+ const loader = new LoadOfSheet(blob)
+
+ // use spread instead of push, because it raises RangeError: Maximum call stack size exceeded when too many docs
+ docs = [...docs, ...(await handleDocumentLoaderDocuments(loader, textSplitter))]
+ }
+
+ docs = handleDocumentLoaderMetadata(docs, _omitMetadataKeys, metadata)
+
+ return handleDocumentLoaderOutput(docs, output)
+ }
+}
+
+module.exports = { nodeClass: MicrosoftExcel_DocumentLoaders }
diff --git a/packages/components/nodes/documentloaders/MicrosoftExcel/excel.svg b/packages/components/nodes/documentloaders/MicrosoftExcel/excel.svg
new file mode 100644
index 00000000000..22d8f9497d7
--- /dev/null
+++ b/packages/components/nodes/documentloaders/MicrosoftExcel/excel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/components/nodes/documentloaders/MicrosoftPowerpoint/MicrosoftPowerpoint.ts b/packages/components/nodes/documentloaders/MicrosoftPowerpoint/MicrosoftPowerpoint.ts
new file mode 100644
index 00000000000..bca5e9a5bc2
--- /dev/null
+++ b/packages/components/nodes/documentloaders/MicrosoftPowerpoint/MicrosoftPowerpoint.ts
@@ -0,0 +1,142 @@
+import { TextSplitter } from 'langchain/text_splitter'
+import { PowerpointLoader } from './PowerpointLoader'
+import { getFileFromStorage, handleDocumentLoaderDocuments, handleDocumentLoaderMetadata, handleDocumentLoaderOutput } from '../../../src'
+import { ICommonObject, IDocument, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface'
+
+class MicrosoftPowerpoint_DocumentLoaders implements INode {
+ label: string
+ name: string
+ version: number
+ description: string
+ type: string
+ icon: string
+ category: string
+ baseClasses: string[]
+ inputs: INodeParams[]
+ outputs: INodeOutputsValue[]
+
+ constructor() {
+ this.label = 'Microsoft PowerPoint'
+ this.name = 'microsoftPowerpoint'
+ this.version = 1.0
+ this.type = 'Document'
+ this.icon = 'powerpoint.svg'
+ this.category = 'Document Loaders'
+ this.description = `Load data from Microsoft PowerPoint files`
+ this.baseClasses = [this.type]
+ this.inputs = [
+ {
+ label: 'PowerPoint File',
+ name: 'powerpointFile',
+ type: 'file',
+ fileType: '.pptx, .ppt'
+ },
+ {
+ label: 'Text Splitter',
+ name: 'textSplitter',
+ type: 'TextSplitter',
+ optional: true
+ },
+ {
+ label: 'Additional Metadata',
+ name: 'metadata',
+ type: 'json',
+ description: 'Additional metadata to be added to the extracted documents',
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Omit Metadata Keys',
+ name: 'omitMetadataKeys',
+ type: 'string',
+ rows: 4,
+ description:
+ 'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field',
+ placeholder: 'key1, key2, key3.nestedKey1',
+ optional: true,
+ additionalParams: true
+ }
+ ]
+ this.outputs = [
+ {
+ label: 'Document',
+ name: 'document',
+ description: 'Array of document objects containing metadata and pageContent',
+ baseClasses: [...this.baseClasses, 'json']
+ },
+ {
+ label: 'Text',
+ name: 'text',
+ description: 'Concatenated string from pageContent of documents',
+ baseClasses: ['string', 'json']
+ }
+ ]
+ }
+
+ getFiles(nodeData: INodeData) {
+ const powerpointFileBase64 = nodeData.inputs?.powerpointFile as string
+
+ let files: string[] = []
+ let fromStorage: boolean = true
+
+ if (powerpointFileBase64.startsWith('FILE-STORAGE::')) {
+ const fileName = powerpointFileBase64.replace('FILE-STORAGE::', '')
+ if (fileName.startsWith('[') && fileName.endsWith(']')) {
+ files = JSON.parse(fileName)
+ } else {
+ files = [fileName]
+ }
+ } else {
+ if (powerpointFileBase64.startsWith('[') && powerpointFileBase64.endsWith(']')) {
+ files = JSON.parse(powerpointFileBase64)
+ } else {
+ files = [powerpointFileBase64]
+ }
+
+ fromStorage = false
+ }
+
+ return { files, fromStorage }
+ }
+
+ async getFileData(file: string, { orgId, chatflowid }: { orgId: string; chatflowid: string }, fromStorage?: boolean) {
+ if (fromStorage) {
+ return getFileFromStorage(file, orgId, chatflowid)
+ } else {
+ const splitDataURI = file.split(',')
+ splitDataURI.pop()
+ return Buffer.from(splitDataURI.pop() || '', 'base64')
+ }
+ }
+
+ async init(nodeData: INodeData, _: string, options: ICommonObject): Promise {
+ const textSplitter = nodeData.inputs?.textSplitter as TextSplitter
+ const metadata = nodeData.inputs?.metadata
+ const output = nodeData.outputs?.output as string
+ const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string
+
+ let docs: IDocument[] = []
+
+ const orgId = options.orgId
+ const chatflowid = options.chatflowid
+
+ const { files, fromStorage } = this.getFiles(nodeData)
+
+ for (const file of files) {
+ if (!file) continue
+
+ const fileData = await this.getFileData(file, { orgId, chatflowid }, fromStorage)
+ const blob = new Blob([fileData])
+ const loader = new PowerpointLoader(blob)
+
+ // use spread instead of push, because it raises RangeError: Maximum call stack size exceeded when too many docs
+ docs = [...docs, ...(await handleDocumentLoaderDocuments(loader, textSplitter))]
+ }
+
+ docs = handleDocumentLoaderMetadata(docs, _omitMetadataKeys, metadata)
+
+ return handleDocumentLoaderOutput(docs, output)
+ }
+}
+
+module.exports = { nodeClass: MicrosoftPowerpoint_DocumentLoaders }
diff --git a/packages/components/nodes/documentloaders/MicrosoftPowerpoint/PowerpointLoader.ts b/packages/components/nodes/documentloaders/MicrosoftPowerpoint/PowerpointLoader.ts
new file mode 100644
index 00000000000..97f266826f0
--- /dev/null
+++ b/packages/components/nodes/documentloaders/MicrosoftPowerpoint/PowerpointLoader.ts
@@ -0,0 +1,101 @@
+import { Document } from '@langchain/core/documents'
+import { BufferLoader } from 'langchain/document_loaders/fs/buffer'
+import { parseOfficeAsync } from 'officeparser'
+
+/**
+ * Document loader that uses officeparser to load PowerPoint documents.
+ *
+ * Each slide is parsed into a separate Document with metadata including
+ * slide number and extracted text content.
+ */
+export class PowerpointLoader extends BufferLoader {
+ attributes: { name: string; description: string; type: string }[] = []
+
+ constructor(filePathOrBlob: string | Blob) {
+ super(filePathOrBlob)
+ this.attributes = []
+ }
+
+ /**
+ * Parse PowerPoint document
+ *
+ * @param raw Raw data Buffer
+ * @param metadata Document metadata
+ * @returns Array of Documents
+ */
+ async parse(raw: Buffer, metadata: Document['metadata']): Promise {
+ const result: Document[] = []
+
+ this.attributes = [
+ { name: 'slideNumber', description: 'Slide number', type: 'number' },
+ { name: 'documentType', description: 'Type of document', type: 'string' }
+ ]
+
+ try {
+ // Use officeparser to extract text from PowerPoint
+ const data = await parseOfficeAsync(raw)
+
+ if (typeof data === 'string' && data.trim()) {
+ // Split content by common slide separators or use the entire content as one document
+ const slides = this.splitIntoSlides(data)
+
+ slides.forEach((slideContent, index) => {
+ if (slideContent.trim()) {
+ result.push({
+ pageContent: slideContent.trim(),
+ metadata: {
+ slideNumber: index + 1,
+ documentType: 'powerpoint',
+ ...metadata
+ }
+ })
+ }
+ })
+ }
+ } catch (error) {
+ console.error('Error parsing PowerPoint file:', error)
+ throw new Error(`Failed to parse PowerPoint file: ${error instanceof Error ? error.message : 'Unknown error'}`)
+ }
+
+ return result
+ }
+
+ /**
+ * Split content into slides based on common patterns
+ * This is a heuristic approach since officeparser returns plain text
+ */
+ private splitIntoSlides(content: string): string[] {
+ // Try to split by common slide patterns
+ const slidePatterns = [
+ /\n\s*Slide\s+\d+/gi,
+ /\n\s*Page\s+\d+/gi,
+ /\n\s*\d+\s*\/\s*\d+/gi,
+ /\n\s*_{3,}/g, // Underscores as separators
+ /\n\s*-{3,}/g // Dashes as separators
+ ]
+
+ let slides: string[] = []
+
+ // Try each pattern and use the one that creates the most reasonable splits
+ for (const pattern of slidePatterns) {
+ const potentialSlides = content.split(pattern)
+ if (potentialSlides.length > 1 && potentialSlides.length < 100) {
+ // Reasonable number of slides
+ slides = potentialSlides
+ break
+ }
+ }
+
+ // If no good pattern found, split by double newlines as a fallback
+ if (slides.length === 0) {
+ slides = content.split(/\n\s*\n\s*\n/)
+ }
+
+ // If still no good split, treat entire content as one slide
+ if (slides.length === 0 || slides.every((slide) => slide.trim().length < 10)) {
+ slides = [content]
+ }
+
+ return slides.filter((slide) => slide.trim().length > 0)
+ }
+}
diff --git a/packages/components/nodes/documentloaders/MicrosoftPowerpoint/powerpoint.svg b/packages/components/nodes/documentloaders/MicrosoftPowerpoint/powerpoint.svg
new file mode 100644
index 00000000000..4d2f7b2a1ca
--- /dev/null
+++ b/packages/components/nodes/documentloaders/MicrosoftPowerpoint/powerpoint.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/components/nodes/documentloaders/MicrosoftWord/MicrosoftWord.ts b/packages/components/nodes/documentloaders/MicrosoftWord/MicrosoftWord.ts
new file mode 100644
index 00000000000..7d74af25999
--- /dev/null
+++ b/packages/components/nodes/documentloaders/MicrosoftWord/MicrosoftWord.ts
@@ -0,0 +1,142 @@
+import { TextSplitter } from 'langchain/text_splitter'
+import { WordLoader } from './WordLoader'
+import { getFileFromStorage, handleDocumentLoaderDocuments, handleDocumentLoaderMetadata, handleDocumentLoaderOutput } from '../../../src'
+import { ICommonObject, IDocument, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface'
+
+class MicrosoftWord_DocumentLoaders implements INode {
+ label: string
+ name: string
+ version: number
+ description: string
+ type: string
+ icon: string
+ category: string
+ baseClasses: string[]
+ inputs: INodeParams[]
+ outputs: INodeOutputsValue[]
+
+ constructor() {
+ this.label = 'Microsoft Word'
+ this.name = 'microsoftWord'
+ this.version = 1.0
+ this.type = 'Document'
+ this.icon = 'word.svg'
+ this.category = 'Document Loaders'
+ this.description = `Load data from Microsoft Word files`
+ this.baseClasses = [this.type]
+ this.inputs = [
+ {
+ label: 'Word File',
+ name: 'docxFile',
+ type: 'file',
+ fileType: '.docx, .doc'
+ },
+ {
+ label: 'Text Splitter',
+ name: 'textSplitter',
+ type: 'TextSplitter',
+ optional: true
+ },
+ {
+ label: 'Additional Metadata',
+ name: 'metadata',
+ type: 'json',
+ description: 'Additional metadata to be added to the extracted documents',
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Omit Metadata Keys',
+ name: 'omitMetadataKeys',
+ type: 'string',
+ rows: 4,
+ description:
+ 'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field',
+ placeholder: 'key1, key2, key3.nestedKey1',
+ optional: true,
+ additionalParams: true
+ }
+ ]
+ this.outputs = [
+ {
+ label: 'Document',
+ name: 'document',
+ description: 'Array of document objects containing metadata and pageContent',
+ baseClasses: [...this.baseClasses, 'json']
+ },
+ {
+ label: 'Text',
+ name: 'text',
+ description: 'Concatenated string from pageContent of documents',
+ baseClasses: ['string', 'json']
+ }
+ ]
+ }
+
+ getFiles(nodeData: INodeData) {
+ const docxFileBase64 = nodeData.inputs?.docxFile as string
+
+ let files: string[] = []
+ let fromStorage: boolean = true
+
+ if (docxFileBase64.startsWith('FILE-STORAGE::')) {
+ const fileName = docxFileBase64.replace('FILE-STORAGE::', '')
+ if (fileName.startsWith('[') && fileName.endsWith(']')) {
+ files = JSON.parse(fileName)
+ } else {
+ files = [fileName]
+ }
+ } else {
+ if (docxFileBase64.startsWith('[') && docxFileBase64.endsWith(']')) {
+ files = JSON.parse(docxFileBase64)
+ } else {
+ files = [docxFileBase64]
+ }
+
+ fromStorage = false
+ }
+
+ return { files, fromStorage }
+ }
+
+ async getFileData(file: string, { orgId, chatflowid }: { orgId: string; chatflowid: string }, fromStorage?: boolean) {
+ if (fromStorage) {
+ return getFileFromStorage(file, orgId, chatflowid)
+ } else {
+ const splitDataURI = file.split(',')
+ splitDataURI.pop()
+ return Buffer.from(splitDataURI.pop() || '', 'base64')
+ }
+ }
+
+ async init(nodeData: INodeData, _: string, options: ICommonObject): Promise {
+ const textSplitter = nodeData.inputs?.textSplitter as TextSplitter
+ const metadata = nodeData.inputs?.metadata
+ const output = nodeData.outputs?.output as string
+ const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string
+
+ let docs: IDocument[] = []
+
+ const orgId = options.orgId
+ const chatflowid = options.chatflowid
+
+ const { files, fromStorage } = this.getFiles(nodeData)
+
+ for (const file of files) {
+ if (!file) continue
+
+ const fileData = await this.getFileData(file, { orgId, chatflowid }, fromStorage)
+ const blob = new Blob([fileData])
+ const loader = new WordLoader(blob)
+
+ // use spread instead of push, because it raises RangeError: Maximum call stack size exceeded when too many docs
+ docs = [...docs, ...(await handleDocumentLoaderDocuments(loader, textSplitter))]
+ }
+
+ docs = handleDocumentLoaderMetadata(docs, _omitMetadataKeys, metadata)
+
+ return handleDocumentLoaderOutput(docs, output)
+ }
+}
+
+module.exports = { nodeClass: MicrosoftWord_DocumentLoaders }
diff --git a/packages/components/nodes/documentloaders/MicrosoftWord/WordLoader.ts b/packages/components/nodes/documentloaders/MicrosoftWord/WordLoader.ts
new file mode 100644
index 00000000000..640e2c4ff09
--- /dev/null
+++ b/packages/components/nodes/documentloaders/MicrosoftWord/WordLoader.ts
@@ -0,0 +1,108 @@
+import { Document } from '@langchain/core/documents'
+import { BufferLoader } from 'langchain/document_loaders/fs/buffer'
+import { parseOfficeAsync } from 'officeparser'
+
+/**
+ * Document loader that uses officeparser to load Word documents.
+ *
+ * The document is parsed into a single Document with metadata including
+ * document type and extracted text content.
+ */
+export class WordLoader extends BufferLoader {
+ attributes: { name: string; description: string; type: string }[] = []
+
+ constructor(filePathOrBlob: string | Blob) {
+ super(filePathOrBlob)
+ this.attributes = []
+ }
+
+ /**
+ * Parse Word document
+ *
+ * @param raw Raw data Buffer
+ * @param metadata Document metadata
+ * @returns Array of Documents
+ */
+ async parse(raw: Buffer, metadata: Document['metadata']): Promise {
+ const result: Document[] = []
+
+ this.attributes = [
+ { name: 'documentType', description: 'Type of document', type: 'string' },
+ { name: 'pageCount', description: 'Number of pages/sections', type: 'number' }
+ ]
+
+ try {
+ // Use officeparser to extract text from Word document
+ const data = await parseOfficeAsync(raw)
+
+ if (typeof data === 'string' && data.trim()) {
+ // Split content by common page/section separators
+ const sections = this.splitIntoSections(data)
+
+ sections.forEach((sectionContent, index) => {
+ if (sectionContent.trim()) {
+ result.push({
+ pageContent: sectionContent.trim(),
+ metadata: {
+ documentType: 'word',
+ pageNumber: index + 1,
+ ...metadata
+ }
+ })
+ }
+ })
+ }
+ } catch (error) {
+ console.error('Error parsing Word file:', error)
+ throw new Error(`Failed to parse Word file: ${error instanceof Error ? error.message : 'Unknown error'}`)
+ }
+
+ return result
+ }
+
+ /**
+ * Split content into sections based on common patterns
+ * This is a heuristic approach since officeparser returns plain text
+ */
+ private splitIntoSections(content: string): string[] {
+ // Try to split by common section patterns
+ const sectionPatterns = [
+ /\n\s*Page\s+\d+/gi,
+ /\n\s*Section\s+\d+/gi,
+ /\n\s*Chapter\s+\d+/gi,
+ /\n\s*\d+\.\s+/gi, // Numbered sections like "1. ", "2. "
+ /\n\s*[A-Z][A-Z\s]{2,}\n/g, // ALL CAPS headings
+ /\n\s*_{5,}/g, // Long underscores as separators
+ /\n\s*-{5,}/g // Long dashes as separators
+ ]
+
+ let sections: string[] = []
+
+ // Try each pattern and use the one that creates the most reasonable splits
+ for (const pattern of sectionPatterns) {
+ const potentialSections = content.split(pattern)
+ if (potentialSections.length > 1 && potentialSections.length < 50) {
+ // Reasonable number of sections
+ sections = potentialSections
+ break
+ }
+ }
+
+ // If no good pattern found, split by multiple newlines as a fallback
+ if (sections.length === 0) {
+ sections = content.split(/\n\s*\n\s*\n\s*\n/)
+ }
+
+ // If still no good split, split by double newlines
+ if (sections.length === 0 || sections.every((section) => section.trim().length < 20)) {
+ sections = content.split(/\n\s*\n\s*\n/)
+ }
+
+ // If still no good split, treat entire content as one section
+ if (sections.length === 0 || sections.every((section) => section.trim().length < 10)) {
+ sections = [content]
+ }
+
+ return sections.filter((section) => section.trim().length > 0)
+ }
+}
diff --git a/packages/components/nodes/documentloaders/MicrosoftWord/word.svg b/packages/components/nodes/documentloaders/MicrosoftWord/word.svg
new file mode 100644
index 00000000000..dabac0ea598
--- /dev/null
+++ b/packages/components/nodes/documentloaders/MicrosoftWord/word.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/components/nodes/documentloaders/S3Directory/S3Directory.ts b/packages/components/nodes/documentloaders/S3Directory/S3Directory.ts
index 072822aef38..4a55869e0e5 100644
--- a/packages/components/nodes/documentloaders/S3Directory/S3Directory.ts
+++ b/packages/components/nodes/documentloaders/S3Directory/S3Directory.ts
@@ -19,9 +19,9 @@ import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'
import { DocxLoader } from '@langchain/community/document_loaders/fs/docx'
import { TextLoader } from 'langchain/document_loaders/fs/text'
import { TextSplitter } from 'langchain/text_splitter'
-
import { CSVLoader } from '../Csv/CsvLoader'
-
+import { LoadOfSheet } from '../MicrosoftExcel/ExcelLoader'
+import { PowerpointLoader } from '../MicrosoftPowerpoint/PowerpointLoader'
class S3_DocumentLoaders implements INode {
label: string
name: string
@@ -240,7 +240,13 @@ class S3_DocumentLoaders implements INode {
'.json': (path) => new JSONLoader(path),
'.txt': (path) => new TextLoader(path),
'.csv': (path) => new CSVLoader(path),
+ '.xls': (path) => new LoadOfSheet(path),
+ '.xlsx': (path) => new LoadOfSheet(path),
+ '.xlsm': (path) => new LoadOfSheet(path),
+ '.xlsb': (path) => new LoadOfSheet(path),
'.docx': (path) => new DocxLoader(path),
+ '.ppt': (path) => new PowerpointLoader(path),
+ '.pptx': (path) => new PowerpointLoader(path),
'.pdf': (path) =>
new PDFLoader(path, {
splitPages: pdfUsage !== 'perFile',
diff --git a/packages/components/nodes/documentloaders/S3File/S3File.ts b/packages/components/nodes/documentloaders/S3File/S3File.ts
index 51c1980459c..d77f37e090c 100644
--- a/packages/components/nodes/documentloaders/S3File/S3File.ts
+++ b/packages/components/nodes/documentloaders/S3File/S3File.ts
@@ -14,12 +14,21 @@ import {
handleDocumentLoaderMetadata,
handleDocumentLoaderOutput
} from '../../../src/utils'
-import { S3Client, GetObjectCommand, S3ClientConfig } from '@aws-sdk/client-s3'
+import { S3Client, GetObjectCommand, HeadObjectCommand, S3ClientConfig } from '@aws-sdk/client-s3'
import { getRegions, MODEL_TYPE } from '../../../src/modelLoader'
import { Readable } from 'node:stream'
import * as fsDefault from 'node:fs'
import * as path from 'node:path'
import * as os from 'node:os'
+import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'
+import { DocxLoader } from '@langchain/community/document_loaders/fs/docx'
+import { CSVLoader } from '@langchain/community/document_loaders/fs/csv'
+import { LoadOfSheet } from '../MicrosoftExcel/ExcelLoader'
+import { PowerpointLoader } from '../MicrosoftPowerpoint/PowerpointLoader'
+import { TextSplitter } from 'langchain/text_splitter'
+import { IDocument } from '../../../src/Interface'
+import { omit } from 'lodash'
+import { handleEscapeCharacters } from '../../../src'
class S3_DocumentLoaders implements INode {
label: string
@@ -37,7 +46,7 @@ class S3_DocumentLoaders implements INode {
constructor() {
this.label = 'S3'
this.name = 'S3'
- this.version = 4.0
+ this.version = 5.0
this.type = 'Document'
this.icon = 's3.svg'
this.category = 'Document Loaders'
@@ -70,6 +79,52 @@ class S3_DocumentLoaders implements INode {
loadMethod: 'listRegions',
default: 'us-east-1'
},
+ {
+ label: 'File Processing Method',
+ name: 'fileProcessingMethod',
+ type: 'options',
+ options: [
+ {
+ label: 'Built In Loaders',
+ name: 'builtIn',
+ description: 'Use the built in loaders to process the file.'
+ },
+ {
+ label: 'Unstructured',
+ name: 'unstructured',
+ description: 'Use the Unstructured API to process the file.'
+ }
+ ],
+ default: 'builtIn'
+ },
+ {
+ label: 'Text Splitter',
+ name: 'textSplitter',
+ type: 'TextSplitter',
+ optional: true,
+ show: {
+ fileProcessingMethod: 'builtIn'
+ }
+ },
+ {
+ label: 'Additional Metadata',
+ name: 'metadata',
+ type: 'json',
+ description: 'Additional metadata to be added to the extracted documents',
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Omit Metadata Keys',
+ name: 'omitMetadataKeys',
+ type: 'string',
+ rows: 4,
+ description:
+ 'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field',
+ placeholder: 'key1, key2, key3.nestedKey1',
+ optional: true,
+ additionalParams: true
+ },
{
label: 'Unstructured API URL',
name: 'unstructuredAPIUrl',
@@ -77,13 +132,21 @@ class S3_DocumentLoaders implements INode {
'Your Unstructured.io URL. Read more on how to get started',
type: 'string',
placeholder: process.env.UNSTRUCTURED_API_URL || 'http://localhost:8000/general/v0/general',
- optional: !!process.env.UNSTRUCTURED_API_URL
+ optional: !!process.env.UNSTRUCTURED_API_URL,
+ additionalParams: true,
+ show: {
+ fileProcessingMethod: 'unstructured'
+ }
},
{
label: 'Unstructured API KEY',
name: 'unstructuredAPIKey',
type: 'password',
- optional: true
+ optional: true,
+ additionalParams: true,
+ show: {
+ fileProcessingMethod: 'unstructured'
+ }
},
{
label: 'Strategy',
@@ -110,7 +173,10 @@ class S3_DocumentLoaders implements INode {
],
optional: true,
additionalParams: true,
- default: 'auto'
+ default: 'auto',
+ show: {
+ fileProcessingMethod: 'unstructured'
+ }
},
{
label: 'Encoding',
@@ -119,7 +185,10 @@ class S3_DocumentLoaders implements INode {
type: 'string',
optional: true,
additionalParams: true,
- default: 'utf-8'
+ default: 'utf-8',
+ show: {
+ fileProcessingMethod: 'unstructured'
+ }
},
{
label: 'Skip Infer Table Types',
@@ -214,7 +283,10 @@ class S3_DocumentLoaders implements INode {
],
optional: true,
additionalParams: true,
- default: '["pdf", "jpg", "png"]'
+ default: '["pdf", "jpg", "png"]',
+ show: {
+ fileProcessingMethod: 'unstructured'
+ }
},
{
label: 'Hi-Res Model Name',
@@ -247,7 +319,10 @@ class S3_DocumentLoaders implements INode {
],
optional: true,
additionalParams: true,
- default: 'detectron2_onnx'
+ default: 'detectron2_onnx',
+ show: {
+ fileProcessingMethod: 'unstructured'
+ }
},
{
label: 'Chunking Strategy',
@@ -267,7 +342,10 @@ class S3_DocumentLoaders implements INode {
],
optional: true,
additionalParams: true,
- default: 'by_title'
+ default: 'by_title',
+ show: {
+ fileProcessingMethod: 'unstructured'
+ }
},
{
label: 'OCR Languages',
@@ -337,7 +415,10 @@ class S3_DocumentLoaders implements INode {
}
],
optional: true,
- additionalParams: true
+ additionalParams: true,
+ show: {
+ fileProcessingMethod: 'unstructured'
+ }
},
{
label: 'Source ID Key',
@@ -348,7 +429,10 @@ class S3_DocumentLoaders implements INode {
default: 'source',
placeholder: 'source',
optional: true,
- additionalParams: true
+ additionalParams: true,
+ show: {
+ fileProcessingMethod: 'unstructured'
+ }
},
{
label: 'Coordinates',
@@ -357,7 +441,10 @@ class S3_DocumentLoaders implements INode {
description: 'If true, return coordinates for each element. Default: false.',
optional: true,
additionalParams: true,
- default: false
+ default: false,
+ show: {
+ fileProcessingMethod: 'unstructured'
+ }
},
{
label: 'XML Keep Tags',
@@ -366,7 +453,10 @@ class S3_DocumentLoaders implements INode {
'If True, will retain the XML tags in the output. Otherwise it will simply extract the text from within the tags. Only applies to partition_xml.',
type: 'boolean',
optional: true,
- additionalParams: true
+ additionalParams: true,
+ show: {
+ fileProcessingMethod: 'unstructured'
+ }
},
{
label: 'Include Page Breaks',
@@ -374,15 +464,10 @@ class S3_DocumentLoaders implements INode {
description: 'When true, the output will include page break elements when the filetype supports it.',
type: 'boolean',
optional: true,
- additionalParams: true
- },
- {
- label: 'XML Keep Tags',
- name: 'xmlKeepTags',
- description: 'Whether to keep XML tags in the output.',
- type: 'boolean',
- optional: true,
- additionalParams: true
+ additionalParams: true,
+ show: {
+ fileProcessingMethod: 'unstructured'
+ }
},
{
label: 'Multi-Page Sections',
@@ -390,7 +475,10 @@ class S3_DocumentLoaders implements INode {
description: 'Whether to treat multi-page documents as separate sections.',
type: 'boolean',
optional: true,
- additionalParams: true
+ additionalParams: true,
+ show: {
+ fileProcessingMethod: 'unstructured'
+ }
},
{
label: 'Combine Under N Chars',
@@ -399,7 +487,10 @@ class S3_DocumentLoaders implements INode {
"If chunking strategy is set, combine elements until a section reaches a length of n chars. Default: value of max_characters. Can't exceed value of max_characters.",
type: 'number',
optional: true,
- additionalParams: true
+ additionalParams: true,
+ show: {
+ fileProcessingMethod: 'unstructured'
+ }
},
{
label: 'New After N Chars',
@@ -408,7 +499,10 @@ class S3_DocumentLoaders implements INode {
"If chunking strategy is set, cut off new sections after reaching a length of n chars (soft max). value of max_characters. Can't exceed value of max_characters.",
type: 'number',
optional: true,
- additionalParams: true
+ additionalParams: true,
+ show: {
+ fileProcessingMethod: 'unstructured'
+ }
},
{
label: 'Max Characters',
@@ -418,7 +512,10 @@ class S3_DocumentLoaders implements INode {
type: 'number',
optional: true,
additionalParams: true,
- default: '500'
+ default: '500',
+ show: {
+ fileProcessingMethod: 'unstructured'
+ }
},
{
label: 'Additional Metadata',
@@ -426,7 +523,10 @@ class S3_DocumentLoaders implements INode {
type: 'json',
description: 'Additional metadata to be added to the extracted documents',
optional: true,
- additionalParams: true
+ additionalParams: true,
+ show: {
+ fileProcessingMethod: 'unstructured'
+ }
},
{
label: 'Omit Metadata Keys',
@@ -437,7 +537,10 @@ class S3_DocumentLoaders implements INode {
'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field',
placeholder: 'key1, key2, key3.nestedKey1',
optional: true,
- additionalParams: true
+ additionalParams: true,
+ show: {
+ fileProcessingMethod: 'unstructured'
+ }
}
]
this.outputs = [
@@ -466,28 +569,17 @@ class S3_DocumentLoaders implements INode {
const bucketName = nodeData.inputs?.bucketName as string
const keyName = nodeData.inputs?.keyName as string
const region = nodeData.inputs?.region as string
- const unstructuredAPIUrl = nodeData.inputs?.unstructuredAPIUrl as string
- const unstructuredAPIKey = nodeData.inputs?.unstructuredAPIKey as string
- const strategy = nodeData.inputs?.strategy as UnstructuredLoaderStrategy
- const encoding = nodeData.inputs?.encoding as string
- const coordinates = nodeData.inputs?.coordinates as boolean
- const skipInferTableTypes = nodeData.inputs?.skipInferTableTypes
- ? JSON.parse(nodeData.inputs?.skipInferTableTypes as string)
- : ([] as SkipInferTableTypes[])
- const hiResModelName = nodeData.inputs?.hiResModelName as HiResModelName
- const includePageBreaks = nodeData.inputs?.includePageBreaks as boolean
- const chunkingStrategy = nodeData.inputs?.chunkingStrategy as 'None' | 'by_title'
+ const fileProcessingMethod = nodeData.inputs?.fileProcessingMethod as string
+ const textSplitter = nodeData.inputs?.textSplitter as TextSplitter
const metadata = nodeData.inputs?.metadata
- const sourceIdKey = (nodeData.inputs?.sourceIdKey as string) || 'source'
- const ocrLanguages = nodeData.inputs?.ocrLanguages ? JSON.parse(nodeData.inputs?.ocrLanguages as string) : ([] as string[])
- const xmlKeepTags = nodeData.inputs?.xmlKeepTags as boolean
- const multiPageSections = nodeData.inputs?.multiPageSections as boolean
- const combineUnderNChars = nodeData.inputs?.combineUnderNChars as number
- const newAfterNChars = nodeData.inputs?.newAfterNChars as number
- const maxCharacters = nodeData.inputs?.maxCharacters as number
const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string
const output = nodeData.outputs?.output as string
+ let omitMetadataKeys: string[] = []
+ if (_omitMetadataKeys) {
+ omitMetadataKeys = _omitMetadataKeys.split(',').map((key) => key.trim())
+ }
+
let credentials: S3ClientConfig['credentials'] | undefined
if (nodeData.credential) {
@@ -508,6 +600,162 @@ class S3_DocumentLoaders implements INode {
credentials
}
+ if (fileProcessingMethod === 'builtIn') {
+ return await this.processWithBuiltInLoaders(
+ bucketName,
+ keyName,
+ s3Config,
+ textSplitter,
+ metadata,
+ omitMetadataKeys,
+ _omitMetadataKeys,
+ output
+ )
+ } else {
+ return await this.processWithUnstructured(nodeData, options, bucketName, keyName, s3Config)
+ }
+ }
+
+ private async processWithBuiltInLoaders(
+ bucketName: string,
+ keyName: string,
+ s3Config: S3ClientConfig,
+ textSplitter: TextSplitter,
+ metadata: any,
+ omitMetadataKeys: string[],
+ _omitMetadataKeys: string,
+ output: string
+ ): Promise {
+ let docs: IDocument[] = []
+
+ try {
+ const s3Client = new S3Client(s3Config)
+
+ // Get file metadata to determine content type
+ const headCommand = new HeadObjectCommand({
+ Bucket: bucketName,
+ Key: keyName
+ })
+
+ const headResponse = await s3Client.send(headCommand)
+ const contentType = headResponse.ContentType || this.getMimeTypeFromExtension(keyName)
+
+ // Download the file
+ const getObjectCommand = new GetObjectCommand({
+ Bucket: bucketName,
+ Key: keyName
+ })
+
+ const response = await s3Client.send(getObjectCommand)
+
+ const objectData = await new Promise((resolve, reject) => {
+ const chunks: Buffer[] = []
+
+ if (response.Body instanceof Readable) {
+ response.Body.on('data', (chunk: Buffer) => chunks.push(chunk))
+ response.Body.on('end', () => resolve(Buffer.concat(chunks)))
+ response.Body.on('error', reject)
+ } else {
+ reject(new Error('Response body is not a readable stream.'))
+ }
+ })
+
+ // Process the file based on content type
+ const fileInfo = {
+ id: keyName,
+ name: path.basename(keyName),
+ mimeType: contentType,
+ size: objectData.length,
+ webViewLink: `s3://${bucketName}/${keyName}`,
+ bucketName: bucketName,
+ key: keyName,
+ lastModified: headResponse.LastModified,
+ etag: headResponse.ETag
+ }
+
+ docs = await this.processFile(fileInfo, objectData)
+
+ // Apply text splitter if provided
+ if (textSplitter && docs.length > 0) {
+ docs = await textSplitter.splitDocuments(docs)
+ }
+
+ // Apply metadata transformations
+ if (metadata) {
+ const parsedMetadata = typeof metadata === 'object' ? metadata : JSON.parse(metadata)
+ docs = docs.map((doc) => ({
+ ...doc,
+ metadata:
+ _omitMetadataKeys === '*'
+ ? {
+ ...parsedMetadata
+ }
+ : omit(
+ {
+ ...doc.metadata,
+ ...parsedMetadata
+ },
+ omitMetadataKeys
+ )
+ }))
+ } else {
+ docs = docs.map((doc) => ({
+ ...doc,
+ metadata:
+ _omitMetadataKeys === '*'
+ ? {}
+ : omit(
+ {
+ ...doc.metadata
+ },
+ omitMetadataKeys
+ )
+ }))
+ }
+ } catch (error) {
+ throw new Error(`Failed to load S3 document: ${error.message}`)
+ }
+
+ if (output === 'document') {
+ return docs
+ } else {
+ let finaltext = ''
+ for (const doc of docs) {
+ finaltext += `${doc.pageContent}\n`
+ }
+ return handleEscapeCharacters(finaltext, false)
+ }
+ }
+
+ private async processWithUnstructured(
+ nodeData: INodeData,
+ options: ICommonObject,
+ bucketName: string,
+ keyName: string,
+ s3Config: S3ClientConfig
+ ): Promise {
+ const unstructuredAPIUrl = nodeData.inputs?.unstructuredAPIUrl as string
+ const unstructuredAPIKey = nodeData.inputs?.unstructuredAPIKey as string
+ const strategy = nodeData.inputs?.strategy as UnstructuredLoaderStrategy
+ const encoding = nodeData.inputs?.encoding as string
+ const coordinates = nodeData.inputs?.coordinates as boolean
+ const skipInferTableTypes = nodeData.inputs?.skipInferTableTypes
+ ? JSON.parse(nodeData.inputs?.skipInferTableTypes as string)
+ : ([] as SkipInferTableTypes[])
+ const hiResModelName = nodeData.inputs?.hiResModelName as HiResModelName
+ const includePageBreaks = nodeData.inputs?.includePageBreaks as boolean
+ const chunkingStrategy = nodeData.inputs?.chunkingStrategy as 'None' | 'by_title'
+ const metadata = nodeData.inputs?.metadata
+ const sourceIdKey = (nodeData.inputs?.sourceIdKey as string) || 'source'
+ const ocrLanguages = nodeData.inputs?.ocrLanguages ? JSON.parse(nodeData.inputs?.ocrLanguages as string) : ([] as string[])
+ const xmlKeepTags = nodeData.inputs?.xmlKeepTags as boolean
+ const multiPageSections = nodeData.inputs?.multiPageSections as boolean
+ const combineUnderNChars = nodeData.inputs?.combineUnderNChars as number
+ const newAfterNChars = nodeData.inputs?.newAfterNChars as number
+ const maxCharacters = nodeData.inputs?.maxCharacters as number
+ const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string
+ const output = nodeData.outputs?.output as string
+
const loader = new S3Loader({
bucket: bucketName,
key: keyName,
@@ -586,5 +834,202 @@ class S3_DocumentLoaders implements INode {
return loader.load()
}
+
+ private getMimeTypeFromExtension(fileName: string): string {
+ const extension = path.extname(fileName).toLowerCase()
+ const mimeTypeMap: { [key: string]: string } = {
+ '.pdf': 'application/pdf',
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ '.doc': 'application/msword',
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ '.xls': 'application/vnd.ms-excel',
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ '.ppt': 'application/vnd.ms-powerpoint',
+ '.txt': 'text/plain',
+ '.csv': 'text/csv',
+ '.html': 'text/html',
+ '.htm': 'text/html',
+ '.json': 'application/json',
+ '.xml': 'application/xml',
+ '.md': 'text/markdown'
+ }
+ return mimeTypeMap[extension] || 'application/octet-stream'
+ }
+
+ private async processFile(fileInfo: any, buffer: Buffer): Promise {
+ try {
+ // Handle different file types
+ if (this.isTextBasedFile(fileInfo.mimeType)) {
+ // Process text files directly from buffer
+ const content = buffer.toString('utf-8')
+
+ // Create document with metadata
+ return [
+ {
+ pageContent: content,
+ metadata: {
+ source: fileInfo.webViewLink,
+ fileId: fileInfo.key,
+ fileName: fileInfo.name,
+ mimeType: fileInfo.mimeType,
+ size: fileInfo.size,
+ lastModified: fileInfo.lastModified,
+ etag: fileInfo.etag,
+ bucketName: fileInfo.bucketName
+ }
+ }
+ ]
+ } else if (this.isSupportedBinaryFile(fileInfo.mimeType)) {
+ // Process binary files using loaders
+ return await this.processBinaryFile(fileInfo, buffer)
+ } else {
+ console.warn(`Unsupported file type ${fileInfo.mimeType} for file ${fileInfo.name}`)
+ return []
+ }
+ } catch (error) {
+ console.warn(`Failed to process file ${fileInfo.name}: ${error.message}`)
+ return []
+ }
+ }
+
+ private isTextBasedFile(mimeType: string): boolean {
+ const textBasedMimeTypes = [
+ 'text/plain',
+ 'text/html',
+ 'text/css',
+ 'text/javascript',
+ 'text/csv',
+ 'text/xml',
+ 'application/json',
+ 'application/xml',
+ 'text/markdown',
+ 'text/x-markdown'
+ ]
+ return textBasedMimeTypes.includes(mimeType)
+ }
+
+ private isSupportedBinaryFile(mimeType: string): boolean {
+ const supportedBinaryTypes = [
+ 'application/pdf',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'application/msword',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'application/vnd.ms-excel',
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ 'application/vnd.ms-powerpoint'
+ ]
+ return supportedBinaryTypes.includes(mimeType)
+ }
+
+ private async processBinaryFile(fileInfo: any, buffer: Buffer): Promise {
+ let tempFilePath: string | null = null
+
+ try {
+ // Create temporary file
+ tempFilePath = await this.createTempFile(buffer, fileInfo.name, fileInfo.mimeType)
+
+ let docs: IDocument[] = []
+ const mimeType = fileInfo.mimeType.toLowerCase()
+
+ switch (mimeType) {
+ case 'application/pdf': {
+ const pdfLoader = new PDFLoader(tempFilePath, {
+ // @ts-ignore
+ pdfjs: () => import('pdf-parse/lib/pdf.js/v1.10.100/build/pdf.js')
+ })
+ docs = await pdfLoader.load()
+ break
+ }
+ case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
+ case 'application/msword': {
+ const docxLoader = new DocxLoader(tempFilePath)
+ docs = await docxLoader.load()
+ break
+ }
+ case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
+ case 'application/vnd.ms-excel': {
+ const excelLoader = new LoadOfSheet(tempFilePath)
+ docs = await excelLoader.load()
+ break
+ }
+ case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
+ case 'application/vnd.ms-powerpoint': {
+ const pptxLoader = new PowerpointLoader(tempFilePath)
+ docs = await pptxLoader.load()
+ break
+ }
+ case 'text/csv': {
+ const csvLoader = new CSVLoader(tempFilePath)
+ docs = await csvLoader.load()
+ break
+ }
+ default:
+ throw new Error(`Unsupported binary file type: ${mimeType}`)
+ }
+
+ // Add S3 metadata to each document
+ if (docs.length > 0) {
+ const s3Metadata = {
+ source: fileInfo.webViewLink,
+ fileId: fileInfo.key,
+ fileName: fileInfo.name,
+ mimeType: fileInfo.mimeType,
+ size: fileInfo.size,
+ lastModified: fileInfo.lastModified,
+ etag: fileInfo.etag,
+ bucketName: fileInfo.bucketName,
+ totalPages: docs.length // Total number of pages/sheets in the file
+ }
+
+ return docs.map((doc, index) => ({
+ ...doc,
+ metadata: {
+ ...doc.metadata, // Keep original loader metadata (page numbers, etc.)
+ ...s3Metadata, // Add S3 metadata
+ pageIndex: index // Add page/sheet index
+ }
+ }))
+ }
+
+ return []
+ } catch (error) {
+ throw new Error(`Failed to process binary file: ${error.message}`)
+ } finally {
+ // Clean up temporary file
+ if (tempFilePath && fsDefault.existsSync(tempFilePath)) {
+ try {
+ fsDefault.unlinkSync(tempFilePath)
+ } catch (e) {
+ console.warn(`Failed to delete temporary file: ${tempFilePath}`)
+ }
+ }
+ }
+ }
+
+ private async createTempFile(buffer: Buffer, fileName: string, mimeType: string): Promise {
+ // Get appropriate file extension
+ let extension = path.extname(fileName)
+ if (!extension) {
+ const extensionMap: { [key: string]: string } = {
+ 'application/pdf': '.pdf',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
+ 'application/msword': '.doc',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
+ 'application/vnd.ms-excel': '.xls',
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
+ 'application/vnd.ms-powerpoint': '.ppt',
+ 'text/csv': '.csv'
+ }
+ extension = extensionMap[mimeType] || '.tmp'
+ }
+
+ // Create temporary file
+ const tempDir = os.tmpdir()
+ const tempFileName = `s3_${Date.now()}_${Math.random().toString(36).substring(7)}${extension}`
+ const tempFilePath = path.join(tempDir, tempFileName)
+
+ fsDefault.writeFileSync(tempFilePath, buffer)
+ return tempFilePath
+ }
}
module.exports = { nodeClass: S3_DocumentLoaders }
diff --git a/packages/components/nodes/sequentialagents/Agent/Agent.ts b/packages/components/nodes/sequentialagents/Agent/Agent.ts
index cb4e03e7e15..0ad1372c11d 100644
--- a/packages/components/nodes/sequentialagents/Agent/Agent.ts
+++ b/packages/components/nodes/sequentialagents/Agent/Agent.ts
@@ -22,7 +22,13 @@ import {
IStateWithMessages,
ConversationHistorySelection
} from '../../../src/Interface'
-import { ToolCallingAgentOutputParser, AgentExecutor, SOURCE_DOCUMENTS_PREFIX, ARTIFACTS_PREFIX } from '../../../src/agents'
+import {
+ ToolCallingAgentOutputParser,
+ AgentExecutor,
+ SOURCE_DOCUMENTS_PREFIX,
+ ARTIFACTS_PREFIX,
+ TOOL_ARGS_PREFIX
+} from '../../../src/agents'
import {
extractOutputFromArray,
getInputVariables,
@@ -1041,6 +1047,17 @@ class ToolNode extends RunnableCallable
}
}
+ let toolInput
+ if (typeof output === 'string' && output.includes(TOOL_ARGS_PREFIX)) {
+ const outputArray = output.split(TOOL_ARGS_PREFIX)
+ output = outputArray[0]
+ try {
+ toolInput = JSON.parse(outputArray[1])
+ } catch (e) {
+ console.error('Error parsing tool input from tool')
+ }
+ }
+
return new ToolMessage({
name: tool.name,
content: typeof output === 'string' ? output : JSON.stringify(output),
@@ -1048,11 +1065,11 @@ class ToolNode extends RunnableCallable
additional_kwargs: {
sourceDocuments,
artifacts,
- args: call.args,
+ args: toolInput ?? call.args,
usedTools: [
{
tool: tool.name ?? '',
- toolInput: call.args,
+ toolInput: toolInput ?? call.args,
toolOutput: output
}
]
diff --git a/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts b/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts
index cc6a260cf8b..7ab010c1a69 100644
--- a/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts
+++ b/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts
@@ -12,7 +12,7 @@ import {
import { AIMessage, AIMessageChunk, BaseMessage, ToolMessage } from '@langchain/core/messages'
import { StructuredTool } from '@langchain/core/tools'
import { RunnableConfig } from '@langchain/core/runnables'
-import { ARTIFACTS_PREFIX, SOURCE_DOCUMENTS_PREFIX } from '../../../src/agents'
+import { ARTIFACTS_PREFIX, SOURCE_DOCUMENTS_PREFIX, TOOL_ARGS_PREFIX } from '../../../src/agents'
import { Document } from '@langchain/core/documents'
import { DataSource } from 'typeorm'
import { MessagesState, RunnableCallable, customGet, getVM } from '../commonUtils'
@@ -448,6 +448,17 @@ class ToolNode ext
}
}
+ let toolInput
+ if (typeof output === 'string' && output.includes(TOOL_ARGS_PREFIX)) {
+ const outputArray = output.split(TOOL_ARGS_PREFIX)
+ output = outputArray[0]
+ try {
+ toolInput = JSON.parse(outputArray[1])
+ } catch (e) {
+ console.error('Error parsing tool input from tool')
+ }
+ }
+
return new ToolMessage({
name: tool.name,
content: typeof output === 'string' ? output : JSON.stringify(output),
@@ -455,11 +466,11 @@ class ToolNode ext
additional_kwargs: {
sourceDocuments,
artifacts,
- args: call.args,
+ args: toolInput ?? call.args,
usedTools: [
{
tool: tool.name ?? '',
- toolInput: call.args,
+ toolInput: toolInput ?? call.args,
toolOutput: output
}
]
diff --git a/packages/components/nodes/tools/Gmail/Gmail.ts b/packages/components/nodes/tools/Gmail/Gmail.ts
new file mode 100644
index 00000000000..f164680aa0e
--- /dev/null
+++ b/packages/components/nodes/tools/Gmail/Gmail.ts
@@ -0,0 +1,630 @@
+import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils'
+import { createGmailTools } from './core'
+import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
+
+class Gmail_Tools implements INode {
+ label: string
+ name: string
+ version: number
+ type: string
+ icon: string
+ category: string
+ description: string
+ baseClasses: string[]
+ credential: INodeParams
+ inputs: INodeParams[]
+
+ constructor() {
+ this.label = 'Gmail'
+ this.name = 'gmail'
+ this.version = 1.0
+ this.type = 'Gmail'
+ this.icon = 'gmail.svg'
+ this.category = 'Tools'
+ this.description = 'Perform Gmail operations for drafts, messages, labels, and threads'
+ this.baseClasses = [this.type, 'Tool']
+ this.credential = {
+ label: 'Connect Credential',
+ name: 'credential',
+ type: 'credential',
+ credentialNames: ['gmailOAuth2']
+ }
+ this.inputs = [
+ {
+ label: 'Type',
+ name: 'gmailType',
+ type: 'options',
+ options: [
+ {
+ label: 'Drafts',
+ name: 'drafts'
+ },
+ {
+ label: 'Messages',
+ name: 'messages'
+ },
+ {
+ label: 'Labels',
+ name: 'labels'
+ },
+ {
+ label: 'Threads',
+ name: 'threads'
+ }
+ ]
+ },
+ // Draft Actions
+ {
+ label: 'Draft Actions',
+ name: 'draftActions',
+ type: 'multiOptions',
+ options: [
+ {
+ label: 'List Drafts',
+ name: 'listDrafts'
+ },
+ {
+ label: 'Create Draft',
+ name: 'createDraft'
+ },
+ {
+ label: 'Get Draft',
+ name: 'getDraft'
+ },
+ {
+ label: 'Update Draft',
+ name: 'updateDraft'
+ },
+ {
+ label: 'Send Draft',
+ name: 'sendDraft'
+ },
+ {
+ label: 'Delete Draft',
+ name: 'deleteDraft'
+ }
+ ],
+ show: {
+ gmailType: ['drafts']
+ }
+ },
+ // Message Actions
+ {
+ label: 'Message Actions',
+ name: 'messageActions',
+ type: 'multiOptions',
+ options: [
+ {
+ label: 'List Messages',
+ name: 'listMessages'
+ },
+ {
+ label: 'Get Message',
+ name: 'getMessage'
+ },
+ {
+ label: 'Send Message',
+ name: 'sendMessage'
+ },
+ {
+ label: 'Modify Message',
+ name: 'modifyMessage'
+ },
+ {
+ label: 'Trash Message',
+ name: 'trashMessage'
+ },
+ {
+ label: 'Untrash Message',
+ name: 'untrashMessage'
+ },
+ {
+ label: 'Delete Message',
+ name: 'deleteMessage'
+ }
+ ],
+ show: {
+ gmailType: ['messages']
+ }
+ },
+ // Label Actions
+ {
+ label: 'Label Actions',
+ name: 'labelActions',
+ type: 'multiOptions',
+ options: [
+ {
+ label: 'List Labels',
+ name: 'listLabels'
+ },
+ {
+ label: 'Get Label',
+ name: 'getLabel'
+ },
+ {
+ label: 'Create Label',
+ name: 'createLabel'
+ },
+ {
+ label: 'Update Label',
+ name: 'updateLabel'
+ },
+ {
+ label: 'Delete Label',
+ name: 'deleteLabel'
+ }
+ ],
+ show: {
+ gmailType: ['labels']
+ }
+ },
+ // Thread Actions
+ {
+ label: 'Thread Actions',
+ name: 'threadActions',
+ type: 'multiOptions',
+ options: [
+ {
+ label: 'List Threads',
+ name: 'listThreads'
+ },
+ {
+ label: 'Get Thread',
+ name: 'getThread'
+ },
+ {
+ label: 'Modify Thread',
+ name: 'modifyThread'
+ },
+ {
+ label: 'Trash Thread',
+ name: 'trashThread'
+ },
+ {
+ label: 'Untrash Thread',
+ name: 'untrashThread'
+ },
+ {
+ label: 'Delete Thread',
+ name: 'deleteThread'
+ }
+ ],
+ show: {
+ gmailType: ['threads']
+ }
+ },
+ // DRAFT PARAMETERS
+ // List Drafts Parameters
+ {
+ label: 'Max Results',
+ name: 'draftMaxResults',
+ type: 'number',
+ description: 'Maximum number of drafts to return',
+ default: 100,
+ show: {
+ draftActions: ['listDrafts']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Create Draft Parameters
+ {
+ label: 'To',
+ name: 'draftTo',
+ type: 'string',
+ description: 'Recipient email address(es), comma-separated',
+ placeholder: 'user1@example.com,user2@example.com',
+ show: {
+ draftActions: ['createDraft']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Subject',
+ name: 'draftSubject',
+ type: 'string',
+ description: 'Email subject',
+ placeholder: 'Email Subject',
+ show: {
+ draftActions: ['createDraft']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Body',
+ name: 'draftBody',
+ type: 'string',
+ description: 'Email body content',
+ placeholder: 'Email content',
+ rows: 4,
+ show: {
+ draftActions: ['createDraft']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'CC',
+ name: 'draftCc',
+ type: 'string',
+ description: 'CC email address(es), comma-separated',
+ placeholder: 'cc1@example.com,cc2@example.com',
+ show: {
+ draftActions: ['createDraft']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'BCC',
+ name: 'draftBcc',
+ type: 'string',
+ description: 'BCC email address(es), comma-separated',
+ placeholder: 'bcc1@example.com,bcc2@example.com',
+ show: {
+ draftActions: ['createDraft']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Draft ID for Get/Update/Send/Delete
+ {
+ label: 'Draft ID',
+ name: 'draftId',
+ type: 'string',
+ description: 'ID of the draft',
+ show: {
+ draftActions: ['getDraft', 'updateDraft', 'sendDraft', 'deleteDraft']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Update Draft Parameters
+ {
+ label: 'To (Update)',
+ name: 'draftUpdateTo',
+ type: 'string',
+ description: 'Recipient email address(es), comma-separated',
+ placeholder: 'user1@example.com,user2@example.com',
+ show: {
+ draftActions: ['updateDraft']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Subject (Update)',
+ name: 'draftUpdateSubject',
+ type: 'string',
+ description: 'Email subject',
+ placeholder: 'Email Subject',
+ show: {
+ draftActions: ['updateDraft']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Body (Update)',
+ name: 'draftUpdateBody',
+ type: 'string',
+ description: 'Email body content',
+ placeholder: 'Email content',
+ rows: 4,
+ show: {
+ draftActions: ['updateDraft']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // MESSAGE PARAMETERS
+ // List Messages Parameters
+ {
+ label: 'Max Results',
+ name: 'messageMaxResults',
+ type: 'number',
+ description: 'Maximum number of messages to return',
+ default: 100,
+ show: {
+ messageActions: ['listMessages']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Query',
+ name: 'messageQuery',
+ type: 'string',
+ description: 'Query string for filtering results (Gmail search syntax)',
+ placeholder: 'is:unread from:example@gmail.com',
+ show: {
+ messageActions: ['listMessages']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Send Message Parameters
+ {
+ label: 'To',
+ name: 'messageTo',
+ type: 'string',
+ description: 'Recipient email address(es), comma-separated',
+ placeholder: 'user1@example.com,user2@example.com',
+ show: {
+ messageActions: ['sendMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Subject',
+ name: 'messageSubject',
+ type: 'string',
+ description: 'Email subject',
+ placeholder: 'Email Subject',
+ show: {
+ messageActions: ['sendMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Body',
+ name: 'messageBody',
+ type: 'string',
+ description: 'Email body content',
+ placeholder: 'Email content',
+ rows: 4,
+ show: {
+ messageActions: ['sendMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'CC',
+ name: 'messageCc',
+ type: 'string',
+ description: 'CC email address(es), comma-separated',
+ placeholder: 'cc1@example.com,cc2@example.com',
+ show: {
+ messageActions: ['sendMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'BCC',
+ name: 'messageBcc',
+ type: 'string',
+ description: 'BCC email address(es), comma-separated',
+ placeholder: 'bcc1@example.com,bcc2@example.com',
+ show: {
+ messageActions: ['sendMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Message ID for Get/Modify/Trash/Untrash/Delete
+ {
+ label: 'Message ID',
+ name: 'messageId',
+ type: 'string',
+ description: 'ID of the message',
+ show: {
+ messageActions: ['getMessage', 'modifyMessage', 'trashMessage', 'untrashMessage', 'deleteMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Message Label Modification
+ {
+ label: 'Add Label IDs',
+ name: 'messageAddLabelIds',
+ type: 'string',
+ description: 'Comma-separated label IDs to add',
+ placeholder: 'INBOX,STARRED',
+ show: {
+ messageActions: ['modifyMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Remove Label IDs',
+ name: 'messageRemoveLabelIds',
+ type: 'string',
+ description: 'Comma-separated label IDs to remove',
+ placeholder: 'UNREAD,SPAM',
+ show: {
+ messageActions: ['modifyMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // LABEL PARAMETERS
+ // Create Label Parameters
+ {
+ label: 'Label Name',
+ name: 'labelName',
+ type: 'string',
+ description: 'Name of the label',
+ placeholder: 'Important',
+ show: {
+ labelActions: ['createLabel', 'updateLabel']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Label Color',
+ name: 'labelColor',
+ type: 'string',
+ description: 'Color of the label (hex color code)',
+ placeholder: '#ff0000',
+ show: {
+ labelActions: ['createLabel', 'updateLabel']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Label ID for Get/Update/Delete
+ {
+ label: 'Label ID',
+ name: 'labelId',
+ type: 'string',
+ description: 'ID of the label',
+ show: {
+ labelActions: ['getLabel', 'updateLabel', 'deleteLabel']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // THREAD PARAMETERS
+ // List Threads Parameters
+ {
+ label: 'Max Results',
+ name: 'threadMaxResults',
+ type: 'number',
+ description: 'Maximum number of threads to return',
+ default: 100,
+ show: {
+ threadActions: ['listThreads']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Query',
+ name: 'threadQuery',
+ type: 'string',
+ description: 'Query string for filtering results (Gmail search syntax)',
+ placeholder: 'is:unread from:example@gmail.com',
+ show: {
+ threadActions: ['listThreads']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Thread ID for Get/Modify/Trash/Untrash/Delete
+ {
+ label: 'Thread ID',
+ name: 'threadId',
+ type: 'string',
+ description: 'ID of the thread',
+ show: {
+ threadActions: ['getThread', 'modifyThread', 'trashThread', 'untrashThread', 'deleteThread']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Thread Label Modification
+ {
+ label: 'Add Label IDs',
+ name: 'threadAddLabelIds',
+ type: 'string',
+ description: 'Comma-separated label IDs to add',
+ placeholder: 'INBOX,STARRED',
+ show: {
+ threadActions: ['modifyThread']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Remove Label IDs',
+ name: 'threadRemoveLabelIds',
+ type: 'string',
+ description: 'Comma-separated label IDs to remove',
+ placeholder: 'UNREAD,SPAM',
+ show: {
+ threadActions: ['modifyThread']
+ },
+ additionalParams: true,
+ optional: true
+ }
+ ]
+ }
+
+ async init(nodeData: INodeData, _: string, options: ICommonObject): Promise {
+ let credentialData = await getCredentialData(nodeData.credential ?? '', options)
+ credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options)
+ const accessToken = getCredentialParam('access_token', credentialData, nodeData)
+
+ if (!accessToken) {
+ throw new Error('No access token found in credential')
+ }
+
+ // Get all actions based on type
+ const gmailType = nodeData.inputs?.gmailType as string
+ let actions: string[] = []
+
+ if (gmailType === 'drafts') {
+ actions = convertMultiOptionsToStringArray(nodeData.inputs?.draftActions)
+ } else if (gmailType === 'messages') {
+ actions = convertMultiOptionsToStringArray(nodeData.inputs?.messageActions)
+ } else if (gmailType === 'labels') {
+ actions = convertMultiOptionsToStringArray(nodeData.inputs?.labelActions)
+ } else if (gmailType === 'threads') {
+ actions = convertMultiOptionsToStringArray(nodeData.inputs?.threadActions)
+ }
+
+ const defaultParams = this.transformNodeInputsToToolArgs(nodeData)
+
+ // Create and return tools based on selected actions
+ const tools = createGmailTools({
+ actions,
+ accessToken,
+ defaultParams
+ })
+
+ return tools
+ }
+
+ transformNodeInputsToToolArgs(nodeData: INodeData): Record {
+ // Collect default parameters from inputs
+ const defaultParams: Record = {}
+
+ // Draft parameters
+ if (nodeData.inputs?.draftMaxResults) defaultParams.draftMaxResults = nodeData.inputs.draftMaxResults
+ if (nodeData.inputs?.draftTo) defaultParams.draftTo = nodeData.inputs.draftTo
+ if (nodeData.inputs?.draftSubject) defaultParams.draftSubject = nodeData.inputs.draftSubject
+ if (nodeData.inputs?.draftBody) defaultParams.draftBody = nodeData.inputs.draftBody
+ if (nodeData.inputs?.draftCc) defaultParams.draftCc = nodeData.inputs.draftCc
+ if (nodeData.inputs?.draftBcc) defaultParams.draftBcc = nodeData.inputs.draftBcc
+ if (nodeData.inputs?.draftId) defaultParams.draftId = nodeData.inputs.draftId
+ if (nodeData.inputs?.draftUpdateTo) defaultParams.draftUpdateTo = nodeData.inputs.draftUpdateTo
+ if (nodeData.inputs?.draftUpdateSubject) defaultParams.draftUpdateSubject = nodeData.inputs.draftUpdateSubject
+ if (nodeData.inputs?.draftUpdateBody) defaultParams.draftUpdateBody = nodeData.inputs.draftUpdateBody
+
+ // Message parameters
+ if (nodeData.inputs?.messageMaxResults) defaultParams.messageMaxResults = nodeData.inputs.messageMaxResults
+ if (nodeData.inputs?.messageQuery) defaultParams.messageQuery = nodeData.inputs.messageQuery
+ if (nodeData.inputs?.messageTo) defaultParams.messageTo = nodeData.inputs.messageTo
+ if (nodeData.inputs?.messageSubject) defaultParams.messageSubject = nodeData.inputs.messageSubject
+ if (nodeData.inputs?.messageBody) defaultParams.messageBody = nodeData.inputs.messageBody
+ if (nodeData.inputs?.messageCc) defaultParams.messageCc = nodeData.inputs.messageCc
+ if (nodeData.inputs?.messageBcc) defaultParams.messageBcc = nodeData.inputs.messageBcc
+ if (nodeData.inputs?.messageId) defaultParams.messageId = nodeData.inputs.messageId
+ if (nodeData.inputs?.messageAddLabelIds) defaultParams.messageAddLabelIds = nodeData.inputs.messageAddLabelIds
+ if (nodeData.inputs?.messageRemoveLabelIds) defaultParams.messageRemoveLabelIds = nodeData.inputs.messageRemoveLabelIds
+
+ // Label parameters
+ if (nodeData.inputs?.labelName) defaultParams.labelName = nodeData.inputs.labelName
+ if (nodeData.inputs?.labelColor) defaultParams.labelColor = nodeData.inputs.labelColor
+ if (nodeData.inputs?.labelId) defaultParams.labelId = nodeData.inputs.labelId
+
+ // Thread parameters
+ if (nodeData.inputs?.threadMaxResults) defaultParams.threadMaxResults = nodeData.inputs.threadMaxResults
+ if (nodeData.inputs?.threadQuery) defaultParams.threadQuery = nodeData.inputs.threadQuery
+ if (nodeData.inputs?.threadId) defaultParams.threadId = nodeData.inputs.threadId
+ if (nodeData.inputs?.threadAddLabelIds) defaultParams.threadAddLabelIds = nodeData.inputs.threadAddLabelIds
+ if (nodeData.inputs?.threadRemoveLabelIds) defaultParams.threadRemoveLabelIds = nodeData.inputs.threadRemoveLabelIds
+
+ return defaultParams
+ }
+}
+
+module.exports = { nodeClass: Gmail_Tools }
diff --git a/packages/components/nodes/tools/Gmail/core.ts b/packages/components/nodes/tools/Gmail/core.ts
new file mode 100644
index 00000000000..14d242c84e1
--- /dev/null
+++ b/packages/components/nodes/tools/Gmail/core.ts
@@ -0,0 +1,1199 @@
+import { z } from 'zod'
+import fetch from 'node-fetch'
+import { DynamicStructuredTool } from '../OpenAPIToolkit/core'
+import { TOOL_ARGS_PREFIX } from '../../../src/agents'
+
+export const desc = `Use this when you want to access Gmail API for managing drafts, messages, labels, and threads`
+
+export interface Headers {
+ [key: string]: string
+}
+
+export interface Body {
+ [key: string]: any
+}
+
+export interface RequestParameters {
+ headers?: Headers
+ body?: Body
+ url?: string
+ description?: string
+ name?: string
+ actions?: string[]
+ accessToken?: string
+ defaultParams?: any
+}
+
+// Define schemas for different Gmail operations
+const ListSchema = z.object({
+ maxResults: z.number().optional().default(100).describe('Maximum number of results to return'),
+ query: z.string().optional().describe('Query string for filtering results (Gmail search syntax)')
+})
+
+const CreateDraftSchema = z.object({
+ to: z.string().describe('Recipient email address(es), comma-separated'),
+ subject: z.string().optional().describe('Email subject'),
+ body: z.string().optional().describe('Email body content'),
+ cc: z.string().optional().describe('CC email address(es), comma-separated'),
+ bcc: z.string().optional().describe('BCC email address(es), comma-separated')
+})
+
+const SendMessageSchema = z.object({
+ to: z.string().describe('Recipient email address(es), comma-separated'),
+ subject: z.string().optional().describe('Email subject'),
+ body: z.string().optional().describe('Email body content'),
+ cc: z.string().optional().describe('CC email address(es), comma-separated'),
+ bcc: z.string().optional().describe('BCC email address(es), comma-separated')
+})
+
+const GetByIdSchema = z.object({
+ id: z.string().describe('ID of the resource')
+})
+
+const ModifySchema = z.object({
+ id: z.string().describe('ID of the resource'),
+ addLabelIds: z.array(z.string()).optional().describe('Label IDs to add'),
+ removeLabelIds: z.array(z.string()).optional().describe('Label IDs to remove')
+})
+
+const CreateLabelSchema = z.object({
+ labelName: z.string().describe('Name of the label'),
+ labelColor: z.string().optional().describe('Color of the label (hex color code)')
+})
+
+class BaseGmailTool extends DynamicStructuredTool {
+ protected accessToken: string = ''
+
+ constructor(args: any) {
+ super(args)
+ this.accessToken = args.accessToken ?? ''
+ }
+
+ async makeGmailRequest(url: string, method: string = 'GET', body?: any, params?: any): Promise {
+ const headers = {
+ Authorization: `Bearer ${this.accessToken}`,
+ 'Content-Type': 'application/json',
+ ...this.headers
+ }
+
+ const response = await fetch(url, {
+ method,
+ headers,
+ body: body ? JSON.stringify(body) : undefined
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ throw new Error(`Gmail API Error ${response.status}: ${response.statusText} - ${errorText}`)
+ }
+
+ const data = await response.text()
+ return data + TOOL_ARGS_PREFIX + JSON.stringify(params)
+ }
+
+ createMimeMessage(to: string, subject?: string, body?: string, cc?: string, bcc?: string): string {
+ let message = ''
+
+ message += `To: ${to}\r\n`
+ if (cc) message += `Cc: ${cc}\r\n`
+ if (bcc) message += `Bcc: ${bcc}\r\n`
+ if (subject) message += `Subject: ${subject}\r\n`
+ message += `MIME-Version: 1.0\r\n`
+ message += `Content-Type: text/html; charset=utf-8\r\n`
+ message += `Content-Transfer-Encoding: base64\r\n\r\n`
+
+ if (body) {
+ message += Buffer.from(body, 'utf-8').toString('base64')
+ }
+
+ return Buffer.from(message).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
+ }
+}
+
+// Draft Tools
+class ListDraftsTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'list_drafts',
+ description: 'List drafts in Gmail mailbox',
+ schema: ListSchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/drafts',
+ method: 'GET',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const queryParams = new URLSearchParams()
+
+ if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString())
+ if (params.query) queryParams.append('q', params.query)
+
+ const url = `https://gmail.googleapis.com/gmail/v1/users/me/drafts?${queryParams.toString()}`
+
+ try {
+ const response = await this.makeGmailRequest(url, 'GET', undefined, params)
+ return response
+ } catch (error) {
+ return `Error listing drafts: ${error}`
+ }
+ }
+}
+
+class CreateDraftTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'create_draft',
+ description: 'Create a new draft in Gmail',
+ schema: CreateDraftSchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/drafts',
+ method: 'POST',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const raw = this.createMimeMessage(params.to, params.subject, params.body, params.cc, params.bcc)
+ const draftData = {
+ message: {
+ raw: raw
+ }
+ }
+
+ const url = 'https://gmail.googleapis.com/gmail/v1/users/me/drafts'
+ const response = await this.makeGmailRequest(url, 'POST', draftData, params)
+ return response
+ } catch (error) {
+ return `Error creating draft: ${error}`
+ }
+ }
+}
+
+class GetDraftTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'get_draft',
+ description: 'Get a specific draft from Gmail',
+ schema: GetByIdSchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/drafts',
+ method: 'GET',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const draftId = params.id || params.draftId
+
+ if (!draftId) {
+ return 'Error: Draft ID is required'
+ }
+
+ try {
+ const url = `https://gmail.googleapis.com/gmail/v1/users/me/drafts/${draftId}`
+ const response = await this.makeGmailRequest(url, 'GET', undefined, params)
+ return response
+ } catch (error) {
+ return `Error getting draft: ${error}`
+ }
+ }
+}
+
+class UpdateDraftTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'update_draft',
+ description: 'Update a specific draft in Gmail',
+ schema: CreateDraftSchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/drafts',
+ method: 'PUT',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const draftId = params.id || params.draftId
+
+ if (!draftId) {
+ return 'Error: Draft ID is required'
+ }
+
+ try {
+ const raw = this.createMimeMessage(params.to, params.subject, params.body, params.cc, params.bcc)
+ const draftData = {
+ message: {
+ raw: raw
+ }
+ }
+
+ const url = `https://gmail.googleapis.com/gmail/v1/users/me/drafts/${draftId}`
+ const response = await this.makeGmailRequest(url, 'PUT', draftData, params)
+ return response
+ } catch (error) {
+ return `Error updating draft: ${error}`
+ }
+ }
+}
+
+class SendDraftTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'send_draft',
+ description: 'Send a specific draft from Gmail',
+ schema: GetByIdSchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/drafts/send',
+ method: 'POST',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const draftId = params.id || params.draftId
+
+ if (!draftId) {
+ return 'Error: Draft ID is required'
+ }
+
+ try {
+ const url = 'https://gmail.googleapis.com/gmail/v1/users/me/drafts/send'
+ const response = await this.makeGmailRequest(url, 'POST', { id: draftId }, params)
+ return response
+ } catch (error) {
+ return `Error sending draft: ${error}`
+ }
+ }
+}
+
+class DeleteDraftTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'delete_draft',
+ description: 'Delete a specific draft from Gmail',
+ schema: GetByIdSchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/drafts',
+ method: 'DELETE',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const draftId = params.id || params.draftId
+
+ if (!draftId) {
+ return 'Error: Draft ID is required'
+ }
+
+ try {
+ const url = `https://gmail.googleapis.com/gmail/v1/users/me/drafts/${draftId}`
+ await this.makeGmailRequest(url, 'DELETE', undefined, params)
+ return `Draft ${draftId} deleted successfully`
+ } catch (error) {
+ return `Error deleting draft: ${error}`
+ }
+ }
+}
+
+// Message Tools
+class ListMessagesTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'list_messages',
+ description: 'List messages in Gmail mailbox',
+ schema: ListSchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/messages',
+ method: 'GET',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const queryParams = new URLSearchParams()
+
+ if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString())
+ if (params.query) queryParams.append('q', params.query)
+
+ const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages?${queryParams.toString()}`
+
+ try {
+ const response = await this.makeGmailRequest(url, 'GET', undefined, params)
+ return response
+ } catch (error) {
+ return `Error listing messages: ${error}`
+ }
+ }
+}
+
+class GetMessageTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'get_message',
+ description: 'Get a specific message from Gmail',
+ schema: GetByIdSchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/messages',
+ method: 'GET',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const messageId = params.id || params.messageId
+
+ if (!messageId) {
+ return 'Error: Message ID is required'
+ }
+
+ try {
+ const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}`
+ const response = await this.makeGmailRequest(url, 'GET', undefined, params)
+ return response
+ } catch (error) {
+ return `Error getting message: ${error}`
+ }
+ }
+}
+
+class SendMessageTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'send_message',
+ description: 'Send a new message via Gmail',
+ schema: SendMessageSchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send',
+ method: 'POST',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const raw = this.createMimeMessage(params.to, params.subject, params.body, params.cc, params.bcc)
+ const messageData = {
+ raw: raw
+ }
+
+ const url = 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send'
+ const response = await this.makeGmailRequest(url, 'POST', messageData, params)
+ return response
+ } catch (error) {
+ return `Error sending message: ${error}`
+ }
+ }
+}
+
+class ModifyMessageTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'modify_message',
+ description: 'Modify labels on a message in Gmail',
+ schema: ModifySchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/messages',
+ method: 'POST',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const messageId = params.id || params.messageId
+
+ if (!messageId) {
+ return 'Error: Message ID is required'
+ }
+
+ try {
+ const modifyData: any = {}
+ if (params.addLabelIds && params.addLabelIds.length > 0) {
+ modifyData.addLabelIds = params.addLabelIds
+ }
+ if (params.removeLabelIds && params.removeLabelIds.length > 0) {
+ modifyData.removeLabelIds = params.removeLabelIds
+ }
+
+ const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}/modify`
+ const response = await this.makeGmailRequest(url, 'POST', modifyData, params)
+ return response
+ } catch (error) {
+ return `Error modifying message: ${error}`
+ }
+ }
+}
+
+class TrashMessageTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'trash_message',
+ description: 'Move a message to trash in Gmail',
+ schema: GetByIdSchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/messages',
+ method: 'POST',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const messageId = params.id || params.messageId
+
+ if (!messageId) {
+ return 'Error: Message ID is required'
+ }
+
+ try {
+ const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}/trash`
+ const response = await this.makeGmailRequest(url, 'POST', undefined, params)
+ return response
+ } catch (error) {
+ return `Error moving message to trash: ${error}`
+ }
+ }
+}
+
+class UntrashMessageTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'untrash_message',
+ description: 'Remove a message from trash in Gmail',
+ schema: GetByIdSchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/messages',
+ method: 'POST',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const messageId = params.id || params.messageId
+
+ if (!messageId) {
+ return 'Error: Message ID is required'
+ }
+
+ try {
+ const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}/untrash`
+ const response = await this.makeGmailRequest(url, 'POST', undefined, params)
+ return response
+ } catch (error) {
+ return `Error removing message from trash: ${error}`
+ }
+ }
+}
+
+class DeleteMessageTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'delete_message',
+ description: 'Permanently delete a message from Gmail',
+ schema: GetByIdSchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/messages',
+ method: 'DELETE',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const messageId = params.id || params.messageId
+
+ if (!messageId) {
+ return 'Error: Message ID is required'
+ }
+
+ try {
+ const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}`
+ await this.makeGmailRequest(url, 'DELETE', undefined, params)
+ return `Message ${messageId} deleted successfully`
+ } catch (error) {
+ return `Error deleting message: ${error}`
+ }
+ }
+}
+
+// Label Tools
+class ListLabelsTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'list_labels',
+ description: 'List labels in Gmail mailbox',
+ schema: z.object({}),
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/labels',
+ method: 'GET',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(): Promise {
+ try {
+ const url = 'https://gmail.googleapis.com/gmail/v1/users/me/labels'
+ const response = await this.makeGmailRequest(url, 'GET', undefined, {})
+ return response
+ } catch (error) {
+ return `Error listing labels: ${error}`
+ }
+ }
+}
+
+class GetLabelTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'get_label',
+ description: 'Get a specific label from Gmail',
+ schema: GetByIdSchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/labels',
+ method: 'GET',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const labelId = params.id || params.labelId
+
+ if (!labelId) {
+ return 'Error: Label ID is required'
+ }
+
+ try {
+ const url = `https://gmail.googleapis.com/gmail/v1/users/me/labels/${labelId}`
+ const response = await this.makeGmailRequest(url, 'GET', undefined, params)
+ return response
+ } catch (error) {
+ return `Error getting label: ${error}`
+ }
+ }
+}
+
+class CreateLabelTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'create_label',
+ description: 'Create a new label in Gmail',
+ schema: CreateLabelSchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/labels',
+ method: 'POST',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ if (!params.labelName) {
+ return 'Error: Label name is required'
+ }
+
+ try {
+ const labelData: any = {
+ name: params.labelName,
+ labelListVisibility: 'labelShow',
+ messageListVisibility: 'show'
+ }
+
+ if (params.labelColor) {
+ labelData.color = {
+ backgroundColor: params.labelColor
+ }
+ }
+
+ const url = 'https://gmail.googleapis.com/gmail/v1/users/me/labels'
+ const response = await this.makeGmailRequest(url, 'POST', labelData, params)
+ return response
+ } catch (error) {
+ return `Error creating label: ${error}`
+ }
+ }
+}
+
+class UpdateLabelTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'update_label',
+ description: 'Update a label in Gmail',
+ schema: CreateLabelSchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/labels',
+ method: 'PUT',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const labelId = params.labelId
+
+ if (!labelId) {
+ return 'Error: Label ID is required'
+ }
+
+ try {
+ const labelData: any = {}
+ if (params.labelName) {
+ labelData.name = params.labelName
+ }
+ if (params.labelColor) {
+ labelData.color = {
+ backgroundColor: params.labelColor
+ }
+ }
+
+ const url = `https://gmail.googleapis.com/gmail/v1/users/me/labels/${labelId}`
+ const response = await this.makeGmailRequest(url, 'PUT', labelData, params)
+ return response
+ } catch (error) {
+ return `Error updating label: ${error}`
+ }
+ }
+}
+
+class DeleteLabelTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'delete_label',
+ description: 'Delete a label from Gmail',
+ schema: GetByIdSchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/labels',
+ method: 'DELETE',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const labelId = params.id || params.labelId
+
+ if (!labelId) {
+ return 'Error: Label ID is required'
+ }
+
+ try {
+ const url = `https://gmail.googleapis.com/gmail/v1/users/me/labels/${labelId}`
+ await this.makeGmailRequest(url, 'DELETE', undefined, params)
+ return `Label ${labelId} deleted successfully`
+ } catch (error) {
+ return `Error deleting label: ${error}`
+ }
+ }
+}
+
+// Thread Tools
+class ListThreadsTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'list_threads',
+ description: 'List threads in Gmail mailbox',
+ schema: ListSchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/threads',
+ method: 'GET',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const queryParams = new URLSearchParams()
+
+ if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString())
+ if (params.query) queryParams.append('q', params.query)
+
+ const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads?${queryParams.toString()}`
+
+ try {
+ const response = await this.makeGmailRequest(url, 'GET', undefined, params)
+ return response
+ } catch (error) {
+ return `Error listing threads: ${error}`
+ }
+ }
+}
+
+class GetThreadTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'get_thread',
+ description: 'Get a specific thread from Gmail',
+ schema: GetByIdSchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/threads',
+ method: 'GET',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const threadId = params.id || params.threadId
+
+ if (!threadId) {
+ return 'Error: Thread ID is required'
+ }
+
+ try {
+ const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}`
+ const response = await this.makeGmailRequest(url, 'GET', undefined, params)
+ return response
+ } catch (error) {
+ return `Error getting thread: ${error}`
+ }
+ }
+}
+
+class ModifyThreadTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'modify_thread',
+ description: 'Modify labels on a thread in Gmail',
+ schema: ModifySchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/threads',
+ method: 'POST',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const threadId = params.id || params.threadId
+
+ if (!threadId) {
+ return 'Error: Thread ID is required'
+ }
+
+ try {
+ const modifyData: any = {}
+ if (params.addLabelIds && params.addLabelIds.length > 0) {
+ modifyData.addLabelIds = params.addLabelIds
+ }
+ if (params.removeLabelIds && params.removeLabelIds.length > 0) {
+ modifyData.removeLabelIds = params.removeLabelIds
+ }
+
+ const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}/modify`
+ const response = await this.makeGmailRequest(url, 'POST', modifyData, params)
+ return response
+ } catch (error) {
+ return `Error modifying thread: ${error}`
+ }
+ }
+}
+
+class TrashThreadTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'trash_thread',
+ description: 'Move a thread to trash in Gmail',
+ schema: GetByIdSchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/threads',
+ method: 'POST',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const threadId = params.id || params.threadId
+
+ if (!threadId) {
+ return 'Error: Thread ID is required'
+ }
+
+ try {
+ const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}/trash`
+ const response = await this.makeGmailRequest(url, 'POST', undefined, params)
+ return response
+ } catch (error) {
+ return `Error moving thread to trash: ${error}`
+ }
+ }
+}
+
+class UntrashThreadTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'untrash_thread',
+ description: 'Remove a thread from trash in Gmail',
+ schema: GetByIdSchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/threads',
+ method: 'POST',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const threadId = params.id || params.threadId
+
+ if (!threadId) {
+ return 'Error: Thread ID is required'
+ }
+
+ try {
+ const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}/untrash`
+ const response = await this.makeGmailRequest(url, 'POST', undefined, params)
+ return response
+ } catch (error) {
+ return `Error removing thread from trash: ${error}`
+ }
+ }
+}
+
+class DeleteThreadTool extends BaseGmailTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'delete_thread',
+ description: 'Permanently delete a thread from Gmail',
+ schema: GetByIdSchema,
+ baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/threads',
+ method: 'DELETE',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const threadId = params.id || params.threadId
+
+ if (!threadId) {
+ return 'Error: Thread ID is required'
+ }
+
+ try {
+ const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}`
+ await this.makeGmailRequest(url, 'DELETE', undefined, params)
+ return `Thread ${threadId} deleted successfully`
+ } catch (error) {
+ return `Error deleting thread: ${error}`
+ }
+ }
+}
+
+export const createGmailTools = (args?: RequestParameters): DynamicStructuredTool[] => {
+ const tools: DynamicStructuredTool[] = []
+ const actions = args?.actions || []
+ const accessToken = args?.accessToken || ''
+ const defaultParams = args?.defaultParams || {}
+
+ // Draft tools
+ if (actions.includes('listDrafts')) {
+ tools.push(
+ new ListDraftsTool({
+ accessToken,
+ defaultParams: defaultParams.listDrafts
+ })
+ )
+ }
+
+ if (actions.includes('createDraft')) {
+ tools.push(
+ new CreateDraftTool({
+ accessToken,
+ defaultParams: defaultParams.createDraft
+ })
+ )
+ }
+
+ if (actions.includes('getDraft')) {
+ tools.push(
+ new GetDraftTool({
+ accessToken,
+ defaultParams: defaultParams.getDraft
+ })
+ )
+ }
+
+ if (actions.includes('updateDraft')) {
+ tools.push(
+ new UpdateDraftTool({
+ accessToken,
+ defaultParams: defaultParams.updateDraft
+ })
+ )
+ }
+
+ if (actions.includes('sendDraft')) {
+ tools.push(
+ new SendDraftTool({
+ accessToken,
+ defaultParams: defaultParams.sendDraft
+ })
+ )
+ }
+
+ if (actions.includes('deleteDraft')) {
+ tools.push(
+ new DeleteDraftTool({
+ accessToken,
+ defaultParams: defaultParams.deleteDraft
+ })
+ )
+ }
+
+ // Message tools
+ if (actions.includes('listMessages')) {
+ tools.push(
+ new ListMessagesTool({
+ accessToken,
+ defaultParams: defaultParams.listMessages
+ })
+ )
+ }
+
+ if (actions.includes('getMessage')) {
+ tools.push(
+ new GetMessageTool({
+ accessToken,
+ defaultParams: defaultParams.getMessage
+ })
+ )
+ }
+
+ if (actions.includes('sendMessage')) {
+ tools.push(
+ new SendMessageTool({
+ accessToken,
+ defaultParams: defaultParams.sendMessage
+ })
+ )
+ }
+
+ if (actions.includes('modifyMessage')) {
+ tools.push(
+ new ModifyMessageTool({
+ accessToken,
+ defaultParams: defaultParams.modifyMessage
+ })
+ )
+ }
+
+ if (actions.includes('trashMessage')) {
+ tools.push(
+ new TrashMessageTool({
+ accessToken,
+ defaultParams: defaultParams.trashMessage
+ })
+ )
+ }
+
+ if (actions.includes('untrashMessage')) {
+ tools.push(
+ new UntrashMessageTool({
+ accessToken,
+ defaultParams: defaultParams.untrashMessage
+ })
+ )
+ }
+
+ if (actions.includes('deleteMessage')) {
+ tools.push(
+ new DeleteMessageTool({
+ accessToken,
+ defaultParams: defaultParams.deleteMessage
+ })
+ )
+ }
+
+ // Label tools
+ if (actions.includes('listLabels')) {
+ tools.push(
+ new ListLabelsTool({
+ accessToken,
+ defaultParams: defaultParams.listLabels
+ })
+ )
+ }
+
+ if (actions.includes('getLabel')) {
+ tools.push(
+ new GetLabelTool({
+ accessToken,
+ defaultParams: defaultParams.getLabel
+ })
+ )
+ }
+
+ if (actions.includes('createLabel')) {
+ tools.push(
+ new CreateLabelTool({
+ accessToken,
+ defaultParams: defaultParams.createLabel
+ })
+ )
+ }
+
+ if (actions.includes('updateLabel')) {
+ tools.push(
+ new UpdateLabelTool({
+ accessToken,
+ defaultParams: defaultParams.updateLabel
+ })
+ )
+ }
+
+ if (actions.includes('deleteLabel')) {
+ tools.push(
+ new DeleteLabelTool({
+ accessToken,
+ defaultParams: defaultParams.deleteLabel
+ })
+ )
+ }
+
+ // Thread tools
+ if (actions.includes('listThreads')) {
+ tools.push(
+ new ListThreadsTool({
+ accessToken,
+ defaultParams: defaultParams.listThreads
+ })
+ )
+ }
+
+ if (actions.includes('getThread')) {
+ tools.push(
+ new GetThreadTool({
+ accessToken,
+ defaultParams: defaultParams.getThread
+ })
+ )
+ }
+
+ if (actions.includes('modifyThread')) {
+ tools.push(
+ new ModifyThreadTool({
+ accessToken,
+ defaultParams: defaultParams.modifyThread
+ })
+ )
+ }
+
+ if (actions.includes('trashThread')) {
+ tools.push(
+ new TrashThreadTool({
+ accessToken,
+ defaultParams: defaultParams.trashThread
+ })
+ )
+ }
+
+ if (actions.includes('untrashThread')) {
+ tools.push(
+ new UntrashThreadTool({
+ accessToken,
+ defaultParams: defaultParams.untrashThread
+ })
+ )
+ }
+
+ if (actions.includes('deleteThread')) {
+ tools.push(
+ new DeleteThreadTool({
+ accessToken,
+ defaultParams: defaultParams.deleteThread
+ })
+ )
+ }
+
+ return tools
+}
diff --git a/packages/components/nodes/tools/Gmail/gmail.svg b/packages/components/nodes/tools/Gmail/gmail.svg
new file mode 100644
index 00000000000..3dceea456d6
--- /dev/null
+++ b/packages/components/nodes/tools/Gmail/gmail.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/components/nodes/tools/GoogleCalendar/GoogleCalendar.ts b/packages/components/nodes/tools/GoogleCalendar/GoogleCalendar.ts
new file mode 100644
index 00000000000..38a0a62910f
--- /dev/null
+++ b/packages/components/nodes/tools/GoogleCalendar/GoogleCalendar.ts
@@ -0,0 +1,621 @@
+import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils'
+import { createGoogleCalendarTools } from './core'
+import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
+
+class GoogleCalendar_Tools implements INode {
+ label: string
+ name: string
+ version: number
+ type: string
+ icon: string
+ category: string
+ description: string
+ baseClasses: string[]
+ credential: INodeParams
+ inputs: INodeParams[]
+
+ constructor() {
+ this.label = 'Google Calendar'
+ this.name = 'googleCalendarTool'
+ this.version = 1.0
+ this.type = 'GoogleCalendar'
+ this.icon = 'google-calendar.svg'
+ this.category = 'Tools'
+ this.description = 'Perform Google Calendar operations such as managing events, calendars, and checking availability'
+ this.baseClasses = ['Tool']
+ this.credential = {
+ label: 'Connect Credential',
+ name: 'credential',
+ type: 'credential',
+ credentialNames: ['googleCalendarOAuth2']
+ }
+ this.inputs = [
+ {
+ label: 'Type',
+ name: 'calendarType',
+ type: 'options',
+ description: 'Type of Google Calendar operation',
+ options: [
+ {
+ label: 'Event',
+ name: 'event'
+ },
+ {
+ label: 'Calendar',
+ name: 'calendar'
+ },
+ {
+ label: 'Freebusy',
+ name: 'freebusy'
+ }
+ ]
+ },
+ // Event Actions
+ {
+ label: 'Event Actions',
+ name: 'eventActions',
+ type: 'multiOptions',
+ description: 'Actions to perform',
+ options: [
+ {
+ label: 'List Events',
+ name: 'listEvents'
+ },
+ {
+ label: 'Create Event',
+ name: 'createEvent'
+ },
+ {
+ label: 'Get Event',
+ name: 'getEvent'
+ },
+ {
+ label: 'Update Event',
+ name: 'updateEvent'
+ },
+ {
+ label: 'Delete Event',
+ name: 'deleteEvent'
+ },
+ {
+ label: 'Quick Add Event',
+ name: 'quickAddEvent'
+ }
+ ],
+ show: {
+ calendarType: ['event']
+ }
+ },
+ // Calendar Actions
+ {
+ label: 'Calendar Actions',
+ name: 'calendarActions',
+ type: 'multiOptions',
+ description: 'Actions to perform',
+ options: [
+ {
+ label: 'List Calendars',
+ name: 'listCalendars'
+ },
+ {
+ label: 'Create Calendar',
+ name: 'createCalendar'
+ },
+ {
+ label: 'Get Calendar',
+ name: 'getCalendar'
+ },
+ {
+ label: 'Update Calendar',
+ name: 'updateCalendar'
+ },
+ {
+ label: 'Delete Calendar',
+ name: 'deleteCalendar'
+ },
+ {
+ label: 'Clear Calendar',
+ name: 'clearCalendar'
+ }
+ ],
+ show: {
+ calendarType: ['calendar']
+ }
+ },
+ // Freebusy Actions
+ {
+ label: 'Freebusy Actions',
+ name: 'freebusyActions',
+ type: 'multiOptions',
+ description: 'Actions to perform',
+ options: [
+ {
+ label: 'Query Freebusy',
+ name: 'queryFreebusy'
+ }
+ ],
+ show: {
+ calendarType: ['freebusy']
+ }
+ },
+ // Event Parameters
+ {
+ label: 'Calendar ID',
+ name: 'calendarId',
+ type: 'string',
+ description: 'Calendar ID (use "primary" for primary calendar)',
+ default: 'primary',
+ show: {
+ calendarType: ['event']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Event ID',
+ name: 'eventId',
+ type: 'string',
+ description: 'Event ID for operations on specific events',
+ show: {
+ eventActions: ['getEvent', 'updateEvent', 'deleteEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Summary',
+ name: 'summary',
+ type: 'string',
+ description: 'Event title/summary',
+ show: {
+ eventActions: ['createEvent', 'updateEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Description',
+ name: 'description',
+ type: 'string',
+ description: 'Event description',
+ show: {
+ eventActions: ['createEvent', 'updateEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Location',
+ name: 'location',
+ type: 'string',
+ description: 'Event location',
+ show: {
+ eventActions: ['createEvent', 'updateEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Start Date Time',
+ name: 'startDateTime',
+ type: 'string',
+ description: 'Event start time (ISO 8601 format: 2023-12-25T10:00:00)',
+ show: {
+ eventActions: ['createEvent', 'updateEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'End Date Time',
+ name: 'endDateTime',
+ type: 'string',
+ description: 'Event end time (ISO 8601 format: 2023-12-25T11:00:00)',
+ show: {
+ eventActions: ['createEvent', 'updateEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Time Zone',
+ name: 'timeZone',
+ type: 'string',
+ description: 'Time zone (e.g., America/New_York)',
+ show: {
+ eventActions: ['createEvent', 'updateEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'All Day Event',
+ name: 'allDay',
+ type: 'boolean',
+ description: 'Whether this is an all-day event',
+ show: {
+ eventActions: ['createEvent', 'updateEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Start Date',
+ name: 'startDate',
+ type: 'string',
+ description: 'Start date for all-day events (YYYY-MM-DD format)',
+ show: {
+ eventActions: ['createEvent', 'updateEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'End Date',
+ name: 'endDate',
+ type: 'string',
+ description: 'End date for all-day events (YYYY-MM-DD format)',
+ show: {
+ eventActions: ['createEvent', 'updateEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Attendees',
+ name: 'attendees',
+ type: 'string',
+ description: 'Comma-separated list of attendee emails',
+ show: {
+ eventActions: ['createEvent', 'updateEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Recurrence Rules',
+ name: 'recurrence',
+ type: 'string',
+ description: 'Recurrence rules (RRULE format)',
+ show: {
+ eventActions: ['createEvent', 'updateEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Reminder Minutes',
+ name: 'reminderMinutes',
+ type: 'number',
+ description: 'Minutes before event to send reminder',
+ show: {
+ eventActions: ['createEvent', 'updateEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Visibility',
+ name: 'visibility',
+ type: 'options',
+ description: 'Event visibility',
+ options: [
+ { label: 'Default', name: 'default' },
+ { label: 'Public', name: 'public' },
+ { label: 'Private', name: 'private' },
+ { label: 'Confidential', name: 'confidential' }
+ ],
+ show: {
+ eventActions: ['createEvent', 'updateEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Quick Add Text',
+ name: 'quickAddText',
+ type: 'string',
+ description: 'Natural language text for quick event creation (e.g., "Lunch with John tomorrow at 12pm")',
+ show: {
+ eventActions: ['quickAddEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Time Min',
+ name: 'timeMin',
+ type: 'string',
+ description: 'Lower bound for event search (ISO 8601 format)',
+ show: {
+ eventActions: ['listEvents']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Time Max',
+ name: 'timeMax',
+ type: 'string',
+ description: 'Upper bound for event search (ISO 8601 format)',
+ show: {
+ eventActions: ['listEvents']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Max Results',
+ name: 'maxResults',
+ type: 'number',
+ description: 'Maximum number of events to return',
+ default: 250,
+ show: {
+ eventActions: ['listEvents']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Single Events',
+ name: 'singleEvents',
+ type: 'boolean',
+ description: 'Whether to expand recurring events into instances',
+ default: true,
+ show: {
+ eventActions: ['listEvents']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Order By',
+ name: 'orderBy',
+ type: 'options',
+ description: 'Order of events returned',
+ options: [
+ { label: 'Start Time', name: 'startTime' },
+ { label: 'Updated', name: 'updated' }
+ ],
+ show: {
+ eventActions: ['listEvents']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Query',
+ name: 'query',
+ type: 'string',
+ description: 'Free text search terms',
+ show: {
+ eventActions: ['listEvents']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Calendar Parameters
+ {
+ label: 'Calendar ID',
+ name: 'calendarIdForCalendar',
+ type: 'string',
+ description: 'Calendar ID for operations on specific calendars',
+ show: {
+ calendarActions: ['getCalendar', 'updateCalendar', 'deleteCalendar', 'clearCalendar']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Calendar Summary',
+ name: 'calendarSummary',
+ type: 'string',
+ description: 'Calendar title/name',
+ show: {
+ calendarActions: ['createCalendar', 'updateCalendar']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Calendar Description',
+ name: 'calendarDescription',
+ type: 'string',
+ description: 'Calendar description',
+ show: {
+ calendarActions: ['createCalendar', 'updateCalendar']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Calendar Location',
+ name: 'calendarLocation',
+ type: 'string',
+ description: 'Calendar location',
+ show: {
+ calendarActions: ['createCalendar', 'updateCalendar']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Calendar Time Zone',
+ name: 'calendarTimeZone',
+ type: 'string',
+ description: 'Calendar time zone (e.g., America/New_York)',
+ show: {
+ calendarActions: ['createCalendar', 'updateCalendar']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Show Hidden',
+ name: 'showHidden',
+ type: 'boolean',
+ description: 'Whether to show hidden calendars',
+ show: {
+ calendarActions: ['listCalendars']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Min Access Role',
+ name: 'minAccessRole',
+ type: 'options',
+ description: 'Minimum access role for calendar list',
+ options: [
+ { label: 'Free/Busy Reader', name: 'freeBusyReader' },
+ { label: 'Reader', name: 'reader' },
+ { label: 'Writer', name: 'writer' },
+ { label: 'Owner', name: 'owner' }
+ ],
+ show: {
+ calendarActions: ['listCalendars']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Freebusy Parameters
+ {
+ label: 'Time Min',
+ name: 'freebusyTimeMin',
+ type: 'string',
+ description: 'Lower bound for freebusy query (ISO 8601 format)',
+ show: {
+ freebusyActions: ['queryFreebusy']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Time Max',
+ name: 'freebusyTimeMax',
+ type: 'string',
+ description: 'Upper bound for freebusy query (ISO 8601 format)',
+ show: {
+ freebusyActions: ['queryFreebusy']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Calendar IDs',
+ name: 'calendarIds',
+ type: 'string',
+ description: 'Comma-separated list of calendar IDs to check for free/busy info',
+ show: {
+ freebusyActions: ['queryFreebusy']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Group Expansion Max',
+ name: 'groupExpansionMax',
+ type: 'number',
+ description: 'Maximum number of calendars for which FreeBusy information is to be provided',
+ show: {
+ freebusyActions: ['queryFreebusy']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Calendar Expansion Max',
+ name: 'calendarExpansionMax',
+ type: 'number',
+ description: 'Maximum number of events that can be expanded for each calendar',
+ show: {
+ freebusyActions: ['queryFreebusy']
+ },
+ additionalParams: true,
+ optional: true
+ }
+ ]
+ }
+
+ async init(nodeData: INodeData, _: string, options: ICommonObject): Promise {
+ const calendarType = nodeData.inputs?.calendarType as string
+
+ let credentialData = await getCredentialData(nodeData.credential ?? '', options)
+ credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options)
+ const accessToken = getCredentialParam('access_token', credentialData, nodeData)
+
+ if (!accessToken) {
+ throw new Error('No access token found in credential')
+ }
+
+ // Get all actions based on type
+ let actions: string[] = []
+
+ if (calendarType === 'event') {
+ actions = convertMultiOptionsToStringArray(nodeData.inputs?.eventActions)
+ } else if (calendarType === 'calendar') {
+ actions = convertMultiOptionsToStringArray(nodeData.inputs?.calendarActions)
+ } else if (calendarType === 'freebusy') {
+ actions = convertMultiOptionsToStringArray(nodeData.inputs?.freebusyActions)
+ }
+
+ const defaultParams = this.transformNodeInputsToToolArgs(nodeData)
+
+ const tools = createGoogleCalendarTools({
+ accessToken,
+ actions,
+ defaultParams
+ })
+
+ return tools
+ }
+
+ transformNodeInputsToToolArgs(nodeData: INodeData): Record {
+ // Collect default parameters from inputs
+ const defaultParams: Record = {}
+
+ // Event parameters
+ if (nodeData.inputs?.calendarId) defaultParams.calendarId = nodeData.inputs.calendarId
+ if (nodeData.inputs?.eventId) defaultParams.eventId = nodeData.inputs.eventId
+ if (nodeData.inputs?.summary) defaultParams.summary = nodeData.inputs.summary
+ if (nodeData.inputs?.description) defaultParams.description = nodeData.inputs.description
+ if (nodeData.inputs?.location) defaultParams.location = nodeData.inputs.location
+ if (nodeData.inputs?.startDateTime) defaultParams.startDateTime = nodeData.inputs.startDateTime
+ if (nodeData.inputs?.endDateTime) defaultParams.endDateTime = nodeData.inputs.endDateTime
+ if (nodeData.inputs?.timeZone) defaultParams.timeZone = nodeData.inputs.timeZone
+ if (nodeData.inputs?.allDay !== undefined) defaultParams.allDay = nodeData.inputs.allDay
+ if (nodeData.inputs?.startDate) defaultParams.startDate = nodeData.inputs.startDate
+ if (nodeData.inputs?.endDate) defaultParams.endDate = nodeData.inputs.endDate
+ if (nodeData.inputs?.attendees) defaultParams.attendees = nodeData.inputs.attendees
+ if (nodeData.inputs?.recurrence) defaultParams.recurrence = nodeData.inputs.recurrence
+ if (nodeData.inputs?.reminderMinutes) defaultParams.reminderMinutes = nodeData.inputs.reminderMinutes
+ if (nodeData.inputs?.visibility) defaultParams.visibility = nodeData.inputs.visibility
+ if (nodeData.inputs?.quickAddText) defaultParams.quickAddText = nodeData.inputs.quickAddText
+ if (nodeData.inputs?.timeMin) defaultParams.timeMin = nodeData.inputs.timeMin
+ if (nodeData.inputs?.timeMax) defaultParams.timeMax = nodeData.inputs.timeMax
+ if (nodeData.inputs?.maxResults) defaultParams.maxResults = nodeData.inputs.maxResults
+ if (nodeData.inputs?.singleEvents !== undefined) defaultParams.singleEvents = nodeData.inputs.singleEvents
+ if (nodeData.inputs?.orderBy) defaultParams.orderBy = nodeData.inputs.orderBy
+ if (nodeData.inputs?.query) defaultParams.query = nodeData.inputs.query
+
+ // Calendar parameters
+ if (nodeData.inputs?.calendarIdForCalendar) defaultParams.calendarIdForCalendar = nodeData.inputs.calendarIdForCalendar
+ if (nodeData.inputs?.calendarSummary) defaultParams.calendarSummary = nodeData.inputs.calendarSummary
+ if (nodeData.inputs?.calendarDescription) defaultParams.calendarDescription = nodeData.inputs.calendarDescription
+ if (nodeData.inputs?.calendarLocation) defaultParams.calendarLocation = nodeData.inputs.calendarLocation
+ if (nodeData.inputs?.calendarTimeZone) defaultParams.calendarTimeZone = nodeData.inputs.calendarTimeZone
+ if (nodeData.inputs?.showHidden !== undefined) defaultParams.showHidden = nodeData.inputs.showHidden
+ if (nodeData.inputs?.minAccessRole) defaultParams.minAccessRole = nodeData.inputs.minAccessRole
+
+ // Freebusy parameters
+ if (nodeData.inputs?.freebusyTimeMin) defaultParams.freebusyTimeMin = nodeData.inputs.freebusyTimeMin
+ if (nodeData.inputs?.freebusyTimeMax) defaultParams.freebusyTimeMax = nodeData.inputs.freebusyTimeMax
+ if (nodeData.inputs?.calendarIds) defaultParams.calendarIds = nodeData.inputs.calendarIds
+ if (nodeData.inputs?.groupExpansionMax) defaultParams.groupExpansionMax = nodeData.inputs.groupExpansionMax
+ if (nodeData.inputs?.calendarExpansionMax) defaultParams.calendarExpansionMax = nodeData.inputs.calendarExpansionMax
+
+ return defaultParams
+ }
+}
+
+module.exports = { nodeClass: GoogleCalendar_Tools }
diff --git a/packages/components/nodes/tools/GoogleCalendar/core.ts b/packages/components/nodes/tools/GoogleCalendar/core.ts
new file mode 100644
index 00000000000..1c89c9c8819
--- /dev/null
+++ b/packages/components/nodes/tools/GoogleCalendar/core.ts
@@ -0,0 +1,864 @@
+import { z } from 'zod'
+import fetch from 'node-fetch'
+import { DynamicStructuredTool } from '../OpenAPIToolkit/core'
+import { TOOL_ARGS_PREFIX } from '../../../src/agents'
+
+export const desc = `Use this when you want to access Google Calendar API for managing events and calendars`
+
+export interface Headers {
+ [key: string]: string
+}
+
+export interface Body {
+ [key: string]: any
+}
+
+export interface RequestParameters {
+ headers?: Headers
+ body?: Body
+ url?: string
+ description?: string
+ name?: string
+ actions?: string[]
+ accessToken?: string
+ defaultParams?: any
+}
+
+// Define schemas for different Google Calendar operations
+
+// Event Schemas
+const ListEventsSchema = z.object({
+ calendarId: z.string().default('primary').describe('Calendar ID (use "primary" for primary calendar)'),
+ timeMin: z.string().optional().describe('Lower bound for event search (RFC3339 timestamp)'),
+ timeMax: z.string().optional().describe('Upper bound for event search (RFC3339 timestamp)'),
+ maxResults: z.number().optional().default(250).describe('Maximum number of events to return'),
+ singleEvents: z.boolean().optional().default(true).describe('Whether to expand recurring events into instances'),
+ orderBy: z.enum(['startTime', 'updated']).optional().describe('Order of events returned'),
+ query: z.string().optional().describe('Free text search terms')
+})
+
+const CreateEventSchema = z.object({
+ calendarId: z.string().default('primary').describe('Calendar ID where the event will be created'),
+ summary: z.string().describe('Event title/summary'),
+ description: z.string().optional().describe('Event description'),
+ location: z.string().optional().describe('Event location'),
+ startDateTime: z.string().optional().describe('Event start time (ISO 8601 format)'),
+ endDateTime: z.string().optional().describe('Event end time (ISO 8601 format)'),
+ startDate: z.string().optional().describe('Start date for all-day events (YYYY-MM-DD)'),
+ endDate: z.string().optional().describe('End date for all-day events (YYYY-MM-DD)'),
+ timeZone: z.string().optional().describe('Time zone (e.g., America/New_York)'),
+ attendees: z.string().optional().describe('Comma-separated list of attendee emails'),
+ recurrence: z.string().optional().describe('Recurrence rules (RRULE format)'),
+ reminderMinutes: z.number().optional().describe('Minutes before event to send reminder'),
+ visibility: z.enum(['default', 'public', 'private', 'confidential']).optional().describe('Event visibility')
+})
+
+const GetEventSchema = z.object({
+ calendarId: z.string().default('primary').describe('Calendar ID'),
+ eventId: z.string().describe('Event ID')
+})
+
+const UpdateEventSchema = z.object({
+ calendarId: z.string().default('primary').describe('Calendar ID'),
+ eventId: z.string().describe('Event ID'),
+ summary: z.string().optional().describe('Updated event title/summary'),
+ description: z.string().optional().describe('Updated event description'),
+ location: z.string().optional().describe('Updated event location'),
+ startDateTime: z.string().optional().describe('Updated event start time (ISO 8601 format)'),
+ endDateTime: z.string().optional().describe('Updated event end time (ISO 8601 format)'),
+ startDate: z.string().optional().describe('Updated start date for all-day events (YYYY-MM-DD)'),
+ endDate: z.string().optional().describe('Updated end date for all-day events (YYYY-MM-DD)'),
+ timeZone: z.string().optional().describe('Updated time zone'),
+ attendees: z.string().optional().describe('Updated comma-separated list of attendee emails'),
+ recurrence: z.string().optional().describe('Updated recurrence rules'),
+ reminderMinutes: z.number().optional().describe('Updated reminder minutes'),
+ visibility: z.enum(['default', 'public', 'private', 'confidential']).optional().describe('Updated event visibility')
+})
+
+const DeleteEventSchema = z.object({
+ calendarId: z.string().default('primary').describe('Calendar ID'),
+ eventId: z.string().describe('Event ID to delete')
+})
+
+const QuickAddEventSchema = z.object({
+ calendarId: z.string().default('primary').describe('Calendar ID'),
+ quickAddText: z.string().describe('Natural language text for quick event creation')
+})
+
+// Calendar Schemas
+const ListCalendarsSchema = z.object({
+ showHidden: z.boolean().optional().describe('Whether to show hidden calendars'),
+ minAccessRole: z.enum(['freeBusyReader', 'reader', 'writer', 'owner']).optional().describe('Minimum access role')
+})
+
+const CreateCalendarSchema = z.object({
+ summary: z.string().describe('Calendar title/name'),
+ description: z.string().optional().describe('Calendar description'),
+ location: z.string().optional().describe('Calendar location'),
+ timeZone: z.string().optional().describe('Calendar time zone (e.g., America/New_York)')
+})
+
+const GetCalendarSchema = z.object({
+ calendarId: z.string().describe('Calendar ID')
+})
+
+const UpdateCalendarSchema = z.object({
+ calendarId: z.string().describe('Calendar ID'),
+ summary: z.string().optional().describe('Updated calendar title/name'),
+ description: z.string().optional().describe('Updated calendar description'),
+ location: z.string().optional().describe('Updated calendar location'),
+ timeZone: z.string().optional().describe('Updated calendar time zone')
+})
+
+const DeleteCalendarSchema = z.object({
+ calendarId: z.string().describe('Calendar ID to delete')
+})
+
+const ClearCalendarSchema = z.object({
+ calendarId: z.string().describe('Calendar ID to clear (removes all events)')
+})
+
+// Freebusy Schemas
+const QueryFreebusySchema = z.object({
+ timeMin: z.string().describe('Lower bound for freebusy query (RFC3339 timestamp)'),
+ timeMax: z.string().describe('Upper bound for freebusy query (RFC3339 timestamp)'),
+ calendarIds: z.string().describe('Comma-separated list of calendar IDs to check for free/busy info'),
+ groupExpansionMax: z.number().optional().describe('Maximum number of calendars for which FreeBusy information is to be provided'),
+ calendarExpansionMax: z.number().optional().describe('Maximum number of events that can be expanded for each calendar')
+})
+
+class BaseGoogleCalendarTool extends DynamicStructuredTool {
+ protected accessToken: string = ''
+
+ constructor(args: any) {
+ super(args)
+ this.accessToken = args.accessToken ?? ''
+ }
+
+ async makeGoogleCalendarRequest({
+ endpoint,
+ method = 'GET',
+ body,
+ params
+ }: {
+ endpoint: string
+ method?: string
+ body?: any
+ params?: any
+ }): Promise {
+ const url = `https://www.googleapis.com/calendar/v3/${endpoint}`
+
+ const headers = {
+ Authorization: `Bearer ${this.accessToken}`,
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ ...this.headers
+ }
+
+ const response = await fetch(url, {
+ method,
+ headers,
+ body: body ? JSON.stringify(body) : undefined
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ throw new Error(`Google Calendar API Error ${response.status}: ${response.statusText} - ${errorText}`)
+ }
+
+ const data = await response.text()
+ return data + TOOL_ARGS_PREFIX + JSON.stringify(params)
+ }
+}
+
+// Event Tools
+class ListEventsTool extends BaseGoogleCalendarTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'list_events',
+ description: 'List events from Google Calendar',
+ schema: ListEventsSchema,
+ baseUrl: '',
+ method: 'GET',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const queryParams = new URLSearchParams()
+
+ if (params.timeMin) queryParams.append('timeMin', params.timeMin)
+ if (params.timeMax) queryParams.append('timeMax', params.timeMax)
+ if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString())
+ if (params.singleEvents !== undefined) queryParams.append('singleEvents', params.singleEvents.toString())
+ if (params.orderBy) queryParams.append('orderBy', params.orderBy)
+ if (params.query) queryParams.append('q', params.query)
+
+ const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events?${queryParams.toString()}`
+
+ try {
+ const response = await this.makeGoogleCalendarRequest({ endpoint, params })
+ return response
+ } catch (error) {
+ return `Error listing events: ${error}`
+ }
+ }
+}
+
+class CreateEventTool extends BaseGoogleCalendarTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'create_event',
+ description: 'Create a new event in Google Calendar',
+ schema: CreateEventSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const eventData: any = {
+ summary: params.summary
+ }
+
+ if (params.description) eventData.description = params.description
+ if (params.location) eventData.location = params.location
+
+ // Handle date/time
+ if (params.startDate && params.endDate) {
+ // All-day event
+ eventData.start = { date: params.startDate }
+ eventData.end = { date: params.endDate }
+ } else if (params.startDateTime && params.endDateTime) {
+ // Timed event
+ eventData.start = {
+ dateTime: params.startDateTime,
+ timeZone: params.timeZone || 'UTC'
+ }
+ eventData.end = {
+ dateTime: params.endDateTime,
+ timeZone: params.timeZone || 'UTC'
+ }
+ }
+
+ // Handle attendees
+ if (params.attendees) {
+ eventData.attendees = params.attendees.split(',').map((email: string) => ({
+ email: email.trim()
+ }))
+ }
+
+ // Handle recurrence
+ if (params.recurrence) {
+ eventData.recurrence = [params.recurrence]
+ }
+
+ // Handle reminders
+ if (params.reminderMinutes !== undefined) {
+ eventData.reminders = {
+ useDefault: false,
+ overrides: [
+ {
+ method: 'popup',
+ minutes: params.reminderMinutes
+ }
+ ]
+ }
+ }
+
+ if (params.visibility) eventData.visibility = params.visibility
+
+ const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events`
+ const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'POST', body: eventData, params })
+ return response
+ } catch (error) {
+ return `Error creating event: ${error}`
+ }
+ }
+}
+
+class GetEventTool extends BaseGoogleCalendarTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'get_event',
+ description: 'Get a specific event from Google Calendar',
+ schema: GetEventSchema,
+ baseUrl: '',
+ method: 'GET',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events/${encodeURIComponent(params.eventId)}`
+ const response = await this.makeGoogleCalendarRequest({ endpoint, params })
+ return response
+ } catch (error) {
+ return `Error getting event: ${error}`
+ }
+ }
+}
+
+class UpdateEventTool extends BaseGoogleCalendarTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'update_event',
+ description: 'Update an existing event in Google Calendar',
+ schema: UpdateEventSchema,
+ baseUrl: '',
+ method: 'PUT',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const updateData: any = {}
+
+ if (params.summary) updateData.summary = params.summary
+ if (params.description) updateData.description = params.description
+ if (params.location) updateData.location = params.location
+
+ // Handle date/time updates
+ if (params.startDate && params.endDate) {
+ updateData.start = { date: params.startDate }
+ updateData.end = { date: params.endDate }
+ } else if (params.startDateTime && params.endDateTime) {
+ updateData.start = {
+ dateTime: params.startDateTime,
+ timeZone: params.timeZone || 'UTC'
+ }
+ updateData.end = {
+ dateTime: params.endDateTime,
+ timeZone: params.timeZone || 'UTC'
+ }
+ }
+
+ if (params.attendees) {
+ updateData.attendees = params.attendees.split(',').map((email: string) => ({
+ email: email.trim()
+ }))
+ }
+
+ if (params.recurrence) {
+ updateData.recurrence = [params.recurrence]
+ }
+
+ if (params.reminderMinutes !== undefined) {
+ updateData.reminders = {
+ useDefault: false,
+ overrides: [
+ {
+ method: 'popup',
+ minutes: params.reminderMinutes
+ }
+ ]
+ }
+ }
+
+ if (params.visibility) updateData.visibility = params.visibility
+
+ const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events/${encodeURIComponent(params.eventId)}`
+ const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'PUT', body: updateData, params })
+ return response
+ } catch (error) {
+ return `Error updating event: ${error}`
+ }
+ }
+}
+
+class DeleteEventTool extends BaseGoogleCalendarTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'delete_event',
+ description: 'Delete an event from Google Calendar',
+ schema: DeleteEventSchema,
+ baseUrl: '',
+ method: 'DELETE',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events/${encodeURIComponent(params.eventId)}`
+ const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'DELETE', params })
+ return response || 'Event deleted successfully'
+ } catch (error) {
+ return `Error deleting event: ${error}`
+ }
+ }
+}
+
+class QuickAddEventTool extends BaseGoogleCalendarTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'quick_add_event',
+ description: 'Quick add event to Google Calendar using natural language',
+ schema: QuickAddEventSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const queryParams = new URLSearchParams()
+ queryParams.append('text', params.quickAddText)
+
+ const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events/quickAdd?${queryParams.toString()}`
+ const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'POST', params })
+ return response
+ } catch (error) {
+ return `Error quick adding event: ${error}`
+ }
+ }
+}
+
+// Calendar Tools
+class ListCalendarsTool extends BaseGoogleCalendarTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'list_calendars',
+ description: 'List calendars from Google Calendar',
+ schema: ListCalendarsSchema,
+ baseUrl: '',
+ method: 'GET',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const queryParams = new URLSearchParams()
+
+ if (params.showHidden !== undefined) queryParams.append('showHidden', params.showHidden.toString())
+ if (params.minAccessRole) queryParams.append('minAccessRole', params.minAccessRole)
+
+ const endpoint = `users/me/calendarList?${queryParams.toString()}`
+
+ try {
+ const response = await this.makeGoogleCalendarRequest({ endpoint, params })
+ return response
+ } catch (error) {
+ return `Error listing calendars: ${error}`
+ }
+ }
+}
+
+class CreateCalendarTool extends BaseGoogleCalendarTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'create_calendar',
+ description: 'Create a new calendar in Google Calendar',
+ schema: CreateCalendarSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const calendarData: any = {
+ summary: params.summary
+ }
+
+ if (params.description) calendarData.description = params.description
+ if (params.location) calendarData.location = params.location
+ if (params.timeZone) calendarData.timeZone = params.timeZone
+
+ const endpoint = 'calendars'
+ const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'POST', body: calendarData, params })
+ return response
+ } catch (error) {
+ return `Error creating calendar: ${error}`
+ }
+ }
+}
+
+class GetCalendarTool extends BaseGoogleCalendarTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'get_calendar',
+ description: 'Get a specific calendar from Google Calendar',
+ schema: GetCalendarSchema,
+ baseUrl: '',
+ method: 'GET',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const endpoint = `calendars/${encodeURIComponent(params.calendarId)}`
+ const response = await this.makeGoogleCalendarRequest({ endpoint, params })
+ return response
+ } catch (error) {
+ return `Error getting calendar: ${error}`
+ }
+ }
+}
+
+class UpdateCalendarTool extends BaseGoogleCalendarTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'update_calendar',
+ description: 'Update an existing calendar in Google Calendar',
+ schema: UpdateCalendarSchema,
+ baseUrl: '',
+ method: 'PUT',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const updateData: any = {}
+
+ if (params.summary) updateData.summary = params.summary
+ if (params.description) updateData.description = params.description
+ if (params.location) updateData.location = params.location
+ if (params.timeZone) updateData.timeZone = params.timeZone
+
+ const endpoint = `calendars/${encodeURIComponent(params.calendarId)}`
+ const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'PUT', body: updateData, params })
+ return response
+ } catch (error) {
+ return `Error updating calendar: ${error}`
+ }
+ }
+}
+
+class DeleteCalendarTool extends BaseGoogleCalendarTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'delete_calendar',
+ description: 'Delete a calendar from Google Calendar',
+ schema: DeleteCalendarSchema,
+ baseUrl: '',
+ method: 'DELETE',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const endpoint = `calendars/${encodeURIComponent(params.calendarId)}`
+ const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'DELETE', params })
+ return response || 'Calendar deleted successfully'
+ } catch (error) {
+ return `Error deleting calendar: ${error}`
+ }
+ }
+}
+
+class ClearCalendarTool extends BaseGoogleCalendarTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'clear_calendar',
+ description: 'Clear all events from a Google Calendar',
+ schema: ClearCalendarSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/clear`
+ const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'POST', params })
+ return response || 'Calendar cleared successfully'
+ } catch (error) {
+ return `Error clearing calendar: ${error}`
+ }
+ }
+}
+
+// Freebusy Tools
+class QueryFreebusyTool extends BaseGoogleCalendarTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'query_freebusy',
+ description: 'Query free/busy information for a set of calendars',
+ schema: QueryFreebusySchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const freebusyData: any = {
+ timeMin: params.timeMin,
+ timeMax: params.timeMax,
+ items: params.calendarIds.split(',').map((id: string) => ({
+ id: id.trim()
+ }))
+ }
+
+ if (params.groupExpansionMax !== undefined) {
+ freebusyData.groupExpansionMax = params.groupExpansionMax
+ }
+
+ if (params.calendarExpansionMax !== undefined) {
+ freebusyData.calendarExpansionMax = params.calendarExpansionMax
+ }
+
+ const endpoint = 'freeBusy'
+ const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'POST', body: freebusyData, params })
+ return response
+ } catch (error) {
+ return `Error querying freebusy: ${error}`
+ }
+ }
+}
+
+export const createGoogleCalendarTools = (args?: RequestParameters): DynamicStructuredTool[] => {
+ const tools: DynamicStructuredTool[] = []
+ const actions = args?.actions || []
+ const accessToken = args?.accessToken || ''
+ const defaultParams = args?.defaultParams || {}
+
+ // Event tools
+ if (actions.includes('listEvents')) {
+ tools.push(
+ new ListEventsTool({
+ accessToken,
+ defaultParams: defaultParams.listEvents
+ })
+ )
+ }
+
+ if (actions.includes('createEvent')) {
+ tools.push(
+ new CreateEventTool({
+ accessToken,
+ defaultParams: defaultParams.createEvent
+ })
+ )
+ }
+
+ if (actions.includes('getEvent')) {
+ tools.push(
+ new GetEventTool({
+ accessToken,
+ defaultParams: defaultParams.getEvent
+ })
+ )
+ }
+
+ if (actions.includes('updateEvent')) {
+ tools.push(
+ new UpdateEventTool({
+ accessToken,
+ defaultParams: defaultParams.updateEvent
+ })
+ )
+ }
+
+ if (actions.includes('deleteEvent')) {
+ tools.push(
+ new DeleteEventTool({
+ accessToken,
+ defaultParams: defaultParams.deleteEvent
+ })
+ )
+ }
+
+ if (actions.includes('quickAddEvent')) {
+ tools.push(
+ new QuickAddEventTool({
+ accessToken,
+ defaultParams: defaultParams.quickAddEvent
+ })
+ )
+ }
+
+ // Calendar tools
+ if (actions.includes('listCalendars')) {
+ tools.push(
+ new ListCalendarsTool({
+ accessToken,
+ defaultParams: defaultParams.listCalendars
+ })
+ )
+ }
+
+ if (actions.includes('createCalendar')) {
+ tools.push(
+ new CreateCalendarTool({
+ accessToken,
+ defaultParams: defaultParams.createCalendar
+ })
+ )
+ }
+
+ if (actions.includes('getCalendar')) {
+ tools.push(
+ new GetCalendarTool({
+ accessToken,
+ defaultParams: defaultParams.getCalendar
+ })
+ )
+ }
+
+ if (actions.includes('updateCalendar')) {
+ tools.push(
+ new UpdateCalendarTool({
+ accessToken,
+ defaultParams: defaultParams.updateCalendar
+ })
+ )
+ }
+
+ if (actions.includes('deleteCalendar')) {
+ tools.push(
+ new DeleteCalendarTool({
+ accessToken,
+ defaultParams: defaultParams.deleteCalendar
+ })
+ )
+ }
+
+ if (actions.includes('clearCalendar')) {
+ tools.push(
+ new ClearCalendarTool({
+ accessToken,
+ defaultParams: defaultParams.clearCalendar
+ })
+ )
+ }
+
+ // Freebusy tools
+ if (actions.includes('queryFreebusy')) {
+ tools.push(
+ new QueryFreebusyTool({
+ accessToken,
+ defaultParams: defaultParams.queryFreebusy
+ })
+ )
+ }
+
+ return tools
+}
diff --git a/packages/components/nodes/tools/GoogleCalendar/google-calendar.svg b/packages/components/nodes/tools/GoogleCalendar/google-calendar.svg
new file mode 100644
index 00000000000..c5ba2d56f30
--- /dev/null
+++ b/packages/components/nodes/tools/GoogleCalendar/google-calendar.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/components/nodes/tools/GoogleDocs/GoogleDocs.ts b/packages/components/nodes/tools/GoogleDocs/GoogleDocs.ts
new file mode 100644
index 00000000000..296a9dea81e
--- /dev/null
+++ b/packages/components/nodes/tools/GoogleDocs/GoogleDocs.ts
@@ -0,0 +1,253 @@
+import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils'
+import { createGoogleDocsTools } from './core'
+import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
+
+class GoogleDocs_Tools implements INode {
+ label: string
+ name: string
+ version: number
+ type: string
+ icon: string
+ category: string
+ description: string
+ baseClasses: string[]
+ credential: INodeParams
+ inputs: INodeParams[]
+
+ constructor() {
+ this.label = 'Google Docs'
+ this.name = 'googleDocsTool'
+ this.version = 1.0
+ this.type = 'GoogleDocs'
+ this.icon = 'google-docs.svg'
+ this.category = 'Tools'
+ this.description =
+ 'Perform Google Docs operations such as creating, reading, updating, and deleting documents, as well as text manipulation'
+ this.baseClasses = ['Tool']
+ this.credential = {
+ label: 'Connect Credential',
+ name: 'credential',
+ type: 'credential',
+ credentialNames: ['googleDocsOAuth2']
+ }
+ this.inputs = [
+ // Document Actions
+ {
+ label: 'Actions',
+ name: 'actions',
+ type: 'multiOptions',
+ description: 'Actions to perform',
+ options: [
+ {
+ label: 'Create Document',
+ name: 'createDocument'
+ },
+ {
+ label: 'Get Document',
+ name: 'getDocument'
+ },
+ {
+ label: 'Update Document',
+ name: 'updateDocument'
+ },
+ {
+ label: 'Insert Text',
+ name: 'insertText'
+ },
+ {
+ label: 'Replace Text',
+ name: 'replaceText'
+ },
+ {
+ label: 'Append Text',
+ name: 'appendText'
+ },
+ {
+ label: 'Get Text Content',
+ name: 'getTextContent'
+ },
+ {
+ label: 'Insert Image',
+ name: 'insertImage'
+ },
+ {
+ label: 'Create Table',
+ name: 'createTable'
+ }
+ ]
+ },
+ // Document Parameters
+ {
+ label: 'Document ID',
+ name: 'documentId',
+ type: 'string',
+ description: 'Document ID for operations on specific documents',
+ show: {
+ actions: [
+ 'getDocument',
+ 'updateDocument',
+ 'insertText',
+ 'replaceText',
+ 'appendText',
+ 'getTextContent',
+ 'insertImage',
+ 'createTable'
+ ]
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Title',
+ name: 'title',
+ type: 'string',
+ description: 'Document title',
+ show: {
+ actions: ['createDocument']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Text Parameters
+ {
+ label: 'Text',
+ name: 'text',
+ type: 'string',
+ description: 'Text content to insert or append',
+ show: {
+ actions: ['createDocument', 'updateDocument', 'insertText', 'appendText']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Index',
+ name: 'index',
+ type: 'number',
+ description: 'Index where to insert text or media (1-based, default: 1 for beginning)',
+ default: 1,
+ show: {
+ actions: ['createDocument', 'updateDocument', 'insertText', 'insertImage', 'createTable']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Replace Text',
+ name: 'replaceText',
+ type: 'string',
+ description: 'Text to replace',
+ show: {
+ actions: ['updateDocument', 'replaceText']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'New Text',
+ name: 'newText',
+ type: 'string',
+ description: 'New text to replace with',
+ show: {
+ actions: ['updateDocument', 'replaceText']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Match Case',
+ name: 'matchCase',
+ type: 'boolean',
+ description: 'Whether the search should be case-sensitive',
+ default: false,
+ show: {
+ actions: ['updateDocument', 'replaceText']
+ },
+ additionalParams: true,
+ optional: true
+ },
+
+ // Media Parameters
+ {
+ label: 'Image URL',
+ name: 'imageUrl',
+ type: 'string',
+ description: 'URL of the image to insert',
+ show: {
+ actions: ['createDocument', 'updateDocument', 'insertImage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Table Rows',
+ name: 'rows',
+ type: 'number',
+ description: 'Number of rows in the table',
+ show: {
+ actions: ['createDocument', 'updateDocument', 'createTable']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Table Columns',
+ name: 'columns',
+ type: 'number',
+ description: 'Number of columns in the table',
+ show: {
+ actions: ['createDocument', 'updateDocument', 'createTable']
+ },
+ additionalParams: true,
+ optional: true
+ }
+ ]
+ }
+
+ async init(nodeData: INodeData, _: string, options: ICommonObject): Promise {
+ let credentialData = await getCredentialData(nodeData.credential ?? '', options)
+ credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options)
+ const accessToken = getCredentialParam('access_token', credentialData, nodeData)
+
+ if (!accessToken) {
+ throw new Error('No access token found in credential')
+ }
+
+ // Get all actions
+ const actions = convertMultiOptionsToStringArray(nodeData.inputs?.actions)
+
+ const defaultParams = this.transformNodeInputsToToolArgs(nodeData)
+
+ const tools = createGoogleDocsTools({
+ accessToken,
+ actions,
+ defaultParams
+ })
+
+ return tools
+ }
+
+ transformNodeInputsToToolArgs(nodeData: INodeData): Record {
+ const nodeInputs: Record = {}
+
+ // Document parameters
+ if (nodeData.inputs?.documentId) nodeInputs.documentId = nodeData.inputs.documentId
+ if (nodeData.inputs?.title) nodeInputs.title = nodeData.inputs.title
+
+ // Text parameters
+ if (nodeData.inputs?.text) nodeInputs.text = nodeData.inputs.text
+ if (nodeData.inputs?.index) nodeInputs.index = nodeData.inputs.index
+ if (nodeData.inputs?.replaceText) nodeInputs.replaceText = nodeData.inputs.replaceText
+ if (nodeData.inputs?.newText) nodeInputs.newText = nodeData.inputs.newText
+ if (nodeData.inputs?.matchCase !== undefined) nodeInputs.matchCase = nodeData.inputs.matchCase
+
+ // Media parameters
+ if (nodeData.inputs?.imageUrl) nodeInputs.imageUrl = nodeData.inputs.imageUrl
+ if (nodeData.inputs?.rows) nodeInputs.rows = nodeData.inputs.rows
+ if (nodeData.inputs?.columns) nodeInputs.columns = nodeData.inputs.columns
+
+ return nodeInputs
+ }
+}
+
+module.exports = { nodeClass: GoogleDocs_Tools }
diff --git a/packages/components/nodes/tools/GoogleDocs/core.ts b/packages/components/nodes/tools/GoogleDocs/core.ts
new file mode 100644
index 00000000000..2b43114f648
--- /dev/null
+++ b/packages/components/nodes/tools/GoogleDocs/core.ts
@@ -0,0 +1,729 @@
+import { z } from 'zod'
+import fetch from 'node-fetch'
+import { DynamicStructuredTool } from '../OpenAPIToolkit/core'
+import { TOOL_ARGS_PREFIX } from '../../../src/agents'
+
+export const desc = `Use this when you want to access Google Docs API for managing documents`
+
+export interface Headers {
+ [key: string]: string
+}
+
+export interface Body {
+ [key: string]: any
+}
+
+export interface RequestParameters {
+ headers?: Headers
+ body?: Body
+ url?: string
+ description?: string
+ name?: string
+ actions?: string[]
+ accessToken?: string
+ defaultParams?: any
+}
+
+// Define schemas for different Google Docs operations
+
+// Document Schemas
+const CreateDocumentSchema = z.object({
+ title: z.string().describe('Document title'),
+ text: z.string().optional().describe('Text content to insert after creating document'),
+ index: z.number().optional().default(1).describe('Index where to insert text or media (1-based, default: 1 for beginning)'),
+ imageUrl: z.string().optional().describe('URL of the image to insert after creating document'),
+ rows: z.number().optional().describe('Number of rows in the table to create'),
+ columns: z.number().optional().describe('Number of columns in the table to create')
+})
+
+const GetDocumentSchema = z.object({
+ documentId: z.string().describe('Document ID to retrieve')
+})
+
+const UpdateDocumentSchema = z.object({
+ documentId: z.string().describe('Document ID to update'),
+ text: z.string().optional().describe('Text content to insert'),
+ index: z.number().optional().default(1).describe('Index where to insert text or media (1-based, default: 1 for beginning)'),
+ replaceText: z.string().optional().describe('Text to replace'),
+ newText: z.string().optional().describe('New text to replace with'),
+ matchCase: z.boolean().optional().default(false).describe('Whether the search should be case-sensitive'),
+ imageUrl: z.string().optional().describe('URL of the image to insert'),
+ rows: z.number().optional().describe('Number of rows in the table to create'),
+ columns: z.number().optional().describe('Number of columns in the table to create')
+})
+
+const InsertTextSchema = z.object({
+ documentId: z.string().describe('Document ID'),
+ text: z.string().describe('Text to insert'),
+ index: z.number().optional().default(1).describe('Index where to insert text (1-based, default: 1 for beginning)')
+})
+
+const ReplaceTextSchema = z.object({
+ documentId: z.string().describe('Document ID'),
+ replaceText: z.string().describe('Text to replace'),
+ newText: z.string().describe('New text to replace with'),
+ matchCase: z.boolean().optional().default(false).describe('Whether the search should be case-sensitive')
+})
+
+const AppendTextSchema = z.object({
+ documentId: z.string().describe('Document ID'),
+ text: z.string().describe('Text to append to the document')
+})
+
+const GetTextContentSchema = z.object({
+ documentId: z.string().describe('Document ID to get text content from')
+})
+
+const InsertImageSchema = z.object({
+ documentId: z.string().describe('Document ID'),
+ imageUrl: z.string().describe('URL of the image to insert'),
+ index: z.number().optional().default(1).describe('Index where to insert image (1-based)')
+})
+
+const CreateTableSchema = z.object({
+ documentId: z.string().describe('Document ID'),
+ rows: z.number().describe('Number of rows in the table'),
+ columns: z.number().describe('Number of columns in the table'),
+ index: z.number().optional().default(1).describe('Index where to insert table (1-based)')
+})
+
+class BaseGoogleDocsTool extends DynamicStructuredTool {
+ protected accessToken: string = ''
+
+ constructor(args: any) {
+ super(args)
+ this.accessToken = args.accessToken ?? ''
+ }
+
+ async makeGoogleDocsRequest({
+ endpoint,
+ method = 'GET',
+ body,
+ params
+ }: {
+ endpoint: string
+ method?: string
+ body?: any
+ params?: any
+ }): Promise {
+ const url = `https://docs.googleapis.com/v1/${endpoint}`
+
+ const headers = {
+ Authorization: `Bearer ${this.accessToken}`,
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ ...this.headers
+ }
+
+ const response = await fetch(url, {
+ method,
+ headers,
+ body: body ? JSON.stringify(body) : undefined
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ throw new Error(`Google Docs API Error ${response.status}: ${response.statusText} - ${errorText}`)
+ }
+
+ const data = await response.text()
+ return data + TOOL_ARGS_PREFIX + JSON.stringify(params)
+ }
+
+ async makeDriveRequest({
+ endpoint,
+ method = 'GET',
+ body,
+ params
+ }: {
+ endpoint: string
+ method?: string
+ body?: any
+ params?: any
+ }): Promise {
+ const url = `https://www.googleapis.com/drive/v3/${endpoint}`
+
+ const headers = {
+ Authorization: `Bearer ${this.accessToken}`,
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ ...this.headers
+ }
+
+ const response = await fetch(url, {
+ method,
+ headers,
+ body: body ? JSON.stringify(body) : undefined
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ throw new Error(`Google Drive API Error ${response.status}: ${response.statusText} - ${errorText}`)
+ }
+
+ const data = await response.text()
+ return data + TOOL_ARGS_PREFIX + JSON.stringify(params)
+ }
+}
+
+// Document Tools
+class CreateDocumentTool extends BaseGoogleDocsTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'create_document',
+ description: 'Create a new Google Docs document',
+ schema: CreateDocumentSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const documentData = {
+ title: params.title
+ }
+
+ const endpoint = 'documents'
+ const createResponse = await this.makeGoogleDocsRequest({
+ endpoint,
+ method: 'POST',
+ body: documentData,
+ params
+ })
+
+ // Get the document ID from the response
+ const documentResponse = JSON.parse(createResponse.split(TOOL_ARGS_PREFIX)[0])
+ const documentId = documentResponse.documentId
+
+ // Now add content if provided
+ const requests = []
+
+ if (params.text) {
+ requests.push({
+ insertText: {
+ location: {
+ index: params.index || 1
+ },
+ text: params.text
+ }
+ })
+ }
+
+ if (params.imageUrl) {
+ requests.push({
+ insertInlineImage: {
+ location: {
+ index: params.index || 1
+ },
+ uri: params.imageUrl
+ }
+ })
+ }
+
+ if (params.rows && params.columns) {
+ requests.push({
+ insertTable: {
+ location: {
+ index: params.index || 1
+ },
+ rows: params.rows,
+ columns: params.columns
+ }
+ })
+ }
+
+ // If we have content to add, make a batch update
+ if (requests.length > 0) {
+ const updateEndpoint = `documents/${encodeURIComponent(documentId)}:batchUpdate`
+ await this.makeGoogleDocsRequest({
+ endpoint: updateEndpoint,
+ method: 'POST',
+ body: { requests },
+ params: {}
+ })
+ }
+
+ return createResponse
+ } catch (error) {
+ return `Error creating document: ${error}`
+ }
+ }
+}
+
+class GetDocumentTool extends BaseGoogleDocsTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'get_document',
+ description: 'Get a Google Docs document by ID',
+ schema: GetDocumentSchema,
+ baseUrl: '',
+ method: 'GET',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const endpoint = `documents/${encodeURIComponent(params.documentId)}`
+ const response = await this.makeGoogleDocsRequest({ endpoint, params })
+ return response
+ } catch (error) {
+ return `Error getting document: ${error}`
+ }
+ }
+}
+
+class UpdateDocumentTool extends BaseGoogleDocsTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'update_document',
+ description: 'Update a Google Docs document with batch requests',
+ schema: UpdateDocumentSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const requests = []
+
+ // Insert text
+ if (params.text) {
+ requests.push({
+ insertText: {
+ location: {
+ index: params.index || 1
+ },
+ text: params.text
+ }
+ })
+ }
+
+ // Replace text
+ if (params.replaceText && params.newText) {
+ requests.push({
+ replaceAllText: {
+ containsText: {
+ text: params.replaceText,
+ matchCase: params.matchCase || false
+ },
+ replaceText: params.newText
+ }
+ })
+ }
+
+ // Insert image
+ if (params.imageUrl) {
+ requests.push({
+ insertInlineImage: {
+ location: {
+ index: params.index || 1
+ },
+ uri: params.imageUrl
+ }
+ })
+ }
+
+ // Create table
+ if (params.rows && params.columns) {
+ requests.push({
+ insertTable: {
+ location: {
+ index: params.index || 1
+ },
+ rows: params.rows,
+ columns: params.columns
+ }
+ })
+ }
+
+ if (requests.length > 0) {
+ const endpoint = `documents/${encodeURIComponent(params.documentId)}:batchUpdate`
+ const response = await this.makeGoogleDocsRequest({
+ endpoint,
+ method: 'POST',
+ body: { requests },
+ params
+ })
+ return response
+ } else {
+ return `No updates specified` + TOOL_ARGS_PREFIX + JSON.stringify(params)
+ }
+ } catch (error) {
+ return `Error updating document: ${error}`
+ }
+ }
+}
+
+class InsertTextTool extends BaseGoogleDocsTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'insert_text',
+ description: 'Insert text into a Google Docs document',
+ schema: InsertTextSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const requests = [
+ {
+ insertText: {
+ location: {
+ index: params.index
+ },
+ text: params.text
+ }
+ }
+ ]
+
+ const endpoint = `documents/${encodeURIComponent(params.documentId)}:batchUpdate`
+ const response = await this.makeGoogleDocsRequest({
+ endpoint,
+ method: 'POST',
+ body: { requests },
+ params
+ })
+ return response
+ } catch (error) {
+ return `Error inserting text: ${error}`
+ }
+ }
+}
+
+class ReplaceTextTool extends BaseGoogleDocsTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'replace_text',
+ description: 'Replace text in a Google Docs document',
+ schema: ReplaceTextSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const requests = [
+ {
+ replaceAllText: {
+ containsText: {
+ text: params.replaceText,
+ matchCase: params.matchCase
+ },
+ replaceText: params.newText
+ }
+ }
+ ]
+
+ const endpoint = `documents/${encodeURIComponent(params.documentId)}:batchUpdate`
+ const response = await this.makeGoogleDocsRequest({
+ endpoint,
+ method: 'POST',
+ body: { requests },
+ params
+ })
+ return response
+ } catch (error) {
+ return `Error replacing text: ${error}`
+ }
+ }
+}
+
+class AppendTextTool extends BaseGoogleDocsTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'append_text',
+ description: 'Append text to the end of a Google Docs document',
+ schema: AppendTextSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ // First get the document to find the end index
+ const getEndpoint = `documents/${encodeURIComponent(params.documentId)}`
+ const docResponse = await this.makeGoogleDocsRequest({ endpoint: getEndpoint, params: {} })
+ const docData = JSON.parse(docResponse.split(TOOL_ARGS_PREFIX)[0])
+
+ // Get the end index of the document body
+ const endIndex = docData.body.content[docData.body.content.length - 1].endIndex - 1
+
+ const requests = [
+ {
+ insertText: {
+ location: {
+ index: endIndex
+ },
+ text: params.text
+ }
+ }
+ ]
+
+ const endpoint = `documents/${encodeURIComponent(params.documentId)}:batchUpdate`
+ const response = await this.makeGoogleDocsRequest({
+ endpoint,
+ method: 'POST',
+ body: { requests },
+ params
+ })
+ return response
+ } catch (error) {
+ return `Error appending text: ${error}`
+ }
+ }
+}
+
+class GetTextContentTool extends BaseGoogleDocsTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'get_text_content',
+ description: 'Get the text content from a Google Docs document',
+ schema: GetTextContentSchema,
+ baseUrl: '',
+ method: 'GET',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const endpoint = `documents/${encodeURIComponent(params.documentId)}`
+ const response = await this.makeGoogleDocsRequest({ endpoint, params })
+
+ // Extract and return just the text content
+ const docData = JSON.parse(response.split(TOOL_ARGS_PREFIX)[0])
+ let textContent = ''
+
+ const extractText = (element: any) => {
+ if (element.paragraph) {
+ element.paragraph.elements?.forEach((elem: any) => {
+ if (elem.textRun) {
+ textContent += elem.textRun.content
+ }
+ })
+ }
+ }
+
+ docData.body.content?.forEach(extractText)
+
+ return JSON.stringify({ textContent }) + TOOL_ARGS_PREFIX + JSON.stringify(params)
+ } catch (error) {
+ return `Error getting text content: ${error}`
+ }
+ }
+}
+
+class InsertImageTool extends BaseGoogleDocsTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'insert_image',
+ description: 'Insert an image into a Google Docs document',
+ schema: InsertImageSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const requests = [
+ {
+ insertInlineImage: {
+ location: {
+ index: params.index
+ },
+ uri: params.imageUrl
+ }
+ }
+ ]
+
+ const endpoint = `documents/${encodeURIComponent(params.documentId)}:batchUpdate`
+ const response = await this.makeGoogleDocsRequest({
+ endpoint,
+ method: 'POST',
+ body: { requests },
+ params
+ })
+ return response
+ } catch (error) {
+ return `Error inserting image: ${error}`
+ }
+ }
+}
+
+class CreateTableTool extends BaseGoogleDocsTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'create_table',
+ description: 'Create a table in a Google Docs document',
+ schema: CreateTableSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const requests = [
+ {
+ insertTable: {
+ location: {
+ index: params.index
+ },
+ rows: params.rows,
+ columns: params.columns
+ }
+ }
+ ]
+
+ const endpoint = `documents/${encodeURIComponent(params.documentId)}:batchUpdate`
+ const response = await this.makeGoogleDocsRequest({
+ endpoint,
+ method: 'POST',
+ body: { requests },
+ params
+ })
+ return response
+ } catch (error) {
+ return `Error creating table: ${error}`
+ }
+ }
+}
+
+export const createGoogleDocsTools = (args?: RequestParameters): DynamicStructuredTool[] => {
+ const actions = args?.actions || []
+ const tools: DynamicStructuredTool[] = []
+
+ if (actions.includes('createDocument') || actions.length === 0) {
+ tools.push(new CreateDocumentTool(args))
+ }
+
+ if (actions.includes('getDocument') || actions.length === 0) {
+ tools.push(new GetDocumentTool(args))
+ }
+
+ if (actions.includes('updateDocument') || actions.length === 0) {
+ tools.push(new UpdateDocumentTool(args))
+ }
+
+ if (actions.includes('insertText') || actions.length === 0) {
+ tools.push(new InsertTextTool(args))
+ }
+
+ if (actions.includes('replaceText') || actions.length === 0) {
+ tools.push(new ReplaceTextTool(args))
+ }
+
+ if (actions.includes('appendText') || actions.length === 0) {
+ tools.push(new AppendTextTool(args))
+ }
+
+ if (actions.includes('getTextContent') || actions.length === 0) {
+ tools.push(new GetTextContentTool(args))
+ }
+
+ if (actions.includes('insertImage') || actions.length === 0) {
+ tools.push(new InsertImageTool(args))
+ }
+
+ if (actions.includes('createTable') || actions.length === 0) {
+ tools.push(new CreateTableTool(args))
+ }
+
+ return tools
+}
diff --git a/packages/components/nodes/tools/GoogleDocs/google-docs.svg b/packages/components/nodes/tools/GoogleDocs/google-docs.svg
new file mode 100644
index 00000000000..7406241a194
--- /dev/null
+++ b/packages/components/nodes/tools/GoogleDocs/google-docs.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/components/nodes/tools/GoogleDrive/GoogleDrive.ts b/packages/components/nodes/tools/GoogleDrive/GoogleDrive.ts
new file mode 100644
index 00000000000..ec44367dfb1
--- /dev/null
+++ b/packages/components/nodes/tools/GoogleDrive/GoogleDrive.ts
@@ -0,0 +1,663 @@
+import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils'
+import { createGoogleDriveTools } from './core'
+import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
+
+class GoogleDrive_Tools implements INode {
+ label: string
+ name: string
+ version: number
+ type: string
+ icon: string
+ category: string
+ description: string
+ baseClasses: string[]
+ credential: INodeParams
+ inputs: INodeParams[]
+
+ constructor() {
+ this.label = 'Google Drive'
+ this.name = 'googleDriveTool'
+ this.version = 1.0
+ this.type = 'GoogleDrive'
+ this.icon = 'google-drive.svg'
+ this.category = 'Tools'
+ this.description = 'Perform Google Drive operations such as managing files, folders, sharing, and searching'
+ this.baseClasses = ['Tool']
+ this.credential = {
+ label: 'Connect Credential',
+ name: 'credential',
+ type: 'credential',
+ credentialNames: ['googleDriveOAuth2']
+ }
+ this.inputs = [
+ {
+ label: 'Type',
+ name: 'driveType',
+ type: 'options',
+ description: 'Type of Google Drive operation',
+ options: [
+ {
+ label: 'File',
+ name: 'file'
+ },
+ {
+ label: 'Folder',
+ name: 'folder'
+ },
+ {
+ label: 'Search',
+ name: 'search'
+ },
+ {
+ label: 'Share',
+ name: 'share'
+ }
+ ]
+ },
+ // File Actions
+ {
+ label: 'File Actions',
+ name: 'fileActions',
+ type: 'multiOptions',
+ description: 'Actions to perform on files',
+ options: [
+ {
+ label: 'List Files',
+ name: 'listFiles'
+ },
+ {
+ label: 'Get File',
+ name: 'getFile'
+ },
+ {
+ label: 'Create File',
+ name: 'createFile'
+ },
+ {
+ label: 'Update File',
+ name: 'updateFile'
+ },
+ {
+ label: 'Delete File',
+ name: 'deleteFile'
+ },
+ {
+ label: 'Copy File',
+ name: 'copyFile'
+ },
+ {
+ label: 'Download File',
+ name: 'downloadFile'
+ }
+ ],
+ show: {
+ driveType: ['file']
+ }
+ },
+ // Folder Actions
+ {
+ label: 'Folder Actions',
+ name: 'folderActions',
+ type: 'multiOptions',
+ description: 'Actions to perform on folders',
+ options: [
+ {
+ label: 'Create Folder',
+ name: 'createFolder'
+ },
+ {
+ label: 'List Folder Contents',
+ name: 'listFolderContents'
+ },
+ {
+ label: 'Delete Folder',
+ name: 'deleteFolder'
+ }
+ ],
+ show: {
+ driveType: ['folder']
+ }
+ },
+ // Search Actions
+ {
+ label: 'Search Actions',
+ name: 'searchActions',
+ type: 'multiOptions',
+ description: 'Search operations',
+ options: [
+ {
+ label: 'Search Files',
+ name: 'searchFiles'
+ }
+ ],
+ show: {
+ driveType: ['search']
+ }
+ },
+ // Share Actions
+ {
+ label: 'Share Actions',
+ name: 'shareActions',
+ type: 'multiOptions',
+ description: 'Sharing operations',
+ options: [
+ {
+ label: 'Share File',
+ name: 'shareFile'
+ },
+ {
+ label: 'Get Permissions',
+ name: 'getPermissions'
+ },
+ {
+ label: 'Remove Permission',
+ name: 'removePermission'
+ }
+ ],
+ show: {
+ driveType: ['share']
+ }
+ },
+ // File Parameters
+ {
+ label: 'File ID',
+ name: 'fileId',
+ type: 'string',
+ description: 'File ID for file operations',
+ show: {
+ fileActions: ['getFile', 'updateFile', 'deleteFile', 'copyFile', 'downloadFile']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'File ID',
+ name: 'fileId',
+ type: 'string',
+ description: 'File ID for sharing operations',
+ show: {
+ shareActions: ['shareFile', 'getPermissions', 'removePermission']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Folder ID',
+ name: 'folderId',
+ type: 'string',
+ description: 'Folder ID for folder operations',
+ show: {
+ folderActions: ['listFolderContents', 'deleteFolder']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Permission ID',
+ name: 'permissionId',
+ type: 'string',
+ description: 'Permission ID to remove',
+ show: {
+ shareActions: ['removePermission']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'File Name',
+ name: 'fileName',
+ type: 'string',
+ description: 'Name of the file',
+ show: {
+ fileActions: ['createFile', 'copyFile']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Folder Name',
+ name: 'fileName',
+ type: 'string',
+ description: 'Name of the folder',
+ show: {
+ folderActions: ['createFolder']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'File Content',
+ name: 'fileContent',
+ type: 'string',
+ description: 'Content of the file (for text files)',
+ show: {
+ fileActions: ['createFile']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'MIME Type',
+ name: 'mimeType',
+ type: 'string',
+ description: 'MIME type of the file (e.g., text/plain, application/pdf)',
+ show: {
+ fileActions: ['createFile']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Parent Folder ID',
+ name: 'parentFolderId',
+ type: 'string',
+ description: 'ID of the parent folder (comma-separated for multiple parents)',
+ show: {
+ fileActions: ['createFile', 'copyFile']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Parent Folder ID',
+ name: 'parentFolderId',
+ type: 'string',
+ description: 'ID of the parent folder for the new folder',
+ show: {
+ folderActions: ['createFolder']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'File Description',
+ name: 'description',
+ type: 'string',
+ description: 'File description',
+ show: {
+ fileActions: ['createFile', 'updateFile']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Folder Description',
+ name: 'description',
+ type: 'string',
+ description: 'Folder description',
+ show: {
+ folderActions: ['createFolder']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Search Parameters
+ {
+ label: 'Search Query',
+ name: 'searchQuery',
+ type: 'string',
+ description: 'Search query using Google Drive search syntax',
+ show: {
+ searchActions: ['searchFiles']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Max Results',
+ name: 'maxResults',
+ type: 'number',
+ description: 'Maximum number of results to return (1-1000)',
+ default: 10,
+ show: {
+ fileActions: ['listFiles']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Max Results',
+ name: 'maxResults',
+ type: 'number',
+ description: 'Maximum number of results to return (1-1000)',
+ default: 10,
+ show: {
+ searchActions: ['searchFiles']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Order By',
+ name: 'orderBy',
+ type: 'options',
+ description: 'Sort order for file results',
+ options: [
+ {
+ label: 'Name',
+ name: 'name'
+ },
+ {
+ label: 'Created Time',
+ name: 'createdTime'
+ },
+ {
+ label: 'Modified Time',
+ name: 'modifiedTime'
+ },
+ {
+ label: 'Size',
+ name: 'quotaBytesUsed'
+ },
+ {
+ label: 'Folder',
+ name: 'folder'
+ }
+ ],
+ show: {
+ fileActions: ['listFiles']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Order By',
+ name: 'orderBy',
+ type: 'options',
+ description: 'Sort order for search results',
+ options: [
+ {
+ label: 'Name',
+ name: 'name'
+ },
+ {
+ label: 'Created Time',
+ name: 'createdTime'
+ },
+ {
+ label: 'Modified Time',
+ name: 'modifiedTime'
+ },
+ {
+ label: 'Size',
+ name: 'quotaBytesUsed'
+ },
+ {
+ label: 'Folder',
+ name: 'folder'
+ }
+ ],
+ show: {
+ searchActions: ['searchFiles']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Share Parameters
+ {
+ label: 'Share Role',
+ name: 'shareRole',
+ type: 'options',
+ description: 'Permission role for sharing',
+ options: [
+ {
+ label: 'Reader',
+ name: 'reader'
+ },
+ {
+ label: 'Writer',
+ name: 'writer'
+ },
+ {
+ label: 'Commenter',
+ name: 'commenter'
+ },
+ {
+ label: 'Owner',
+ name: 'owner'
+ }
+ ],
+ show: {
+ shareActions: ['shareFile']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Share Type',
+ name: 'shareType',
+ type: 'options',
+ description: 'Type of permission',
+ options: [
+ {
+ label: 'User',
+ name: 'user'
+ },
+ {
+ label: 'Group',
+ name: 'group'
+ },
+ {
+ label: 'Domain',
+ name: 'domain'
+ },
+ {
+ label: 'Anyone',
+ name: 'anyone'
+ }
+ ],
+ show: {
+ shareActions: ['shareFile']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Email Address',
+ name: 'emailAddress',
+ type: 'string',
+ description: 'Email address for user/group sharing',
+ show: {
+ shareActions: ['shareFile']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Domain Name',
+ name: 'domainName',
+ type: 'string',
+ description: 'Domain name for domain sharing',
+ show: {
+ shareActions: ['shareFile']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Send Notification Email',
+ name: 'sendNotificationEmail',
+ type: 'boolean',
+ description: 'Whether to send notification emails when sharing',
+ default: true,
+ show: {
+ shareActions: ['shareFile']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Email Message',
+ name: 'emailMessage',
+ type: 'string',
+ description: 'Custom message to include in notification email',
+ show: {
+ shareActions: ['shareFile']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Advanced Parameters for File Actions
+ {
+ label: 'Include Items From All Drives',
+ name: 'includeItemsFromAllDrives',
+ type: 'boolean',
+ description: 'Include items from all drives (shared drives)',
+ show: {
+ fileActions: ['listFiles']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Include Items From All Drives',
+ name: 'includeItemsFromAllDrives',
+ type: 'boolean',
+ description: 'Include items from all drives (shared drives)',
+ show: {
+ searchActions: ['searchFiles']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Supports All Drives',
+ name: 'supportsAllDrives',
+ type: 'boolean',
+ description: 'Whether the application supports both My Drives and shared drives',
+ show: {
+ fileActions: ['listFiles', 'getFile', 'createFile', 'updateFile', 'deleteFile', 'copyFile', 'downloadFile']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Supports All Drives',
+ name: 'supportsAllDrives',
+ type: 'boolean',
+ description: 'Whether the application supports both My Drives and shared drives',
+ show: {
+ folderActions: ['createFolder', 'listFolderContents', 'deleteFolder']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Supports All Drives',
+ name: 'supportsAllDrives',
+ type: 'boolean',
+ description: 'Whether the application supports both My Drives and shared drives',
+ show: {
+ searchActions: ['searchFiles']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Supports All Drives',
+ name: 'supportsAllDrives',
+ type: 'boolean',
+ description: 'Whether the application supports both My Drives and shared drives',
+ show: {
+ shareActions: ['shareFile', 'getPermissions', 'removePermission']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Fields',
+ name: 'fields',
+ type: 'string',
+ description: 'Specific fields to include in response (e.g., "files(id,name,mimeType)")',
+ show: {
+ fileActions: ['listFiles', 'getFile']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Acknowledge Abuse',
+ name: 'acknowledgeAbuse',
+ type: 'boolean',
+ description: 'Acknowledge the risk of downloading known malware or abusive files',
+ show: {
+ fileActions: ['getFile', 'downloadFile']
+ },
+ additionalParams: true,
+ optional: true
+ }
+ ]
+ }
+
+ async init(nodeData: INodeData, _: string, options: ICommonObject): Promise {
+ let credentialData = await getCredentialData(nodeData.credential ?? '', options)
+ credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options)
+ const accessToken = getCredentialParam('access_token', credentialData, nodeData)
+
+ if (!accessToken) {
+ throw new Error('No access token found in credential')
+ }
+
+ const driveType = nodeData.inputs?.driveType as string
+ const fileActions = convertMultiOptionsToStringArray(nodeData.inputs?.fileActions)
+ const folderActions = convertMultiOptionsToStringArray(nodeData.inputs?.folderActions)
+ const searchActions = convertMultiOptionsToStringArray(nodeData.inputs?.searchActions)
+ const shareActions = convertMultiOptionsToStringArray(nodeData.inputs?.shareActions)
+
+ // Combine all actions based on type
+ let actions: string[] = []
+ if (driveType === 'file') {
+ actions = fileActions
+ } else if (driveType === 'folder') {
+ actions = folderActions
+ } else if (driveType === 'search') {
+ actions = searchActions
+ } else if (driveType === 'share') {
+ actions = shareActions
+ }
+
+ const defaultParams = this.transformNodeInputsToToolArgs(nodeData)
+
+ const tools = createGoogleDriveTools({
+ accessToken,
+ actions,
+ defaultParams
+ })
+
+ return tools
+ }
+
+ transformNodeInputsToToolArgs(nodeData: INodeData): Record {
+ // Collect default parameters from inputs
+ const defaultParams: Record = {}
+
+ // Add parameters based on the inputs provided
+ if (nodeData.inputs?.fileId) defaultParams.fileId = nodeData.inputs.fileId
+ if (nodeData.inputs?.folderId) defaultParams.folderId = nodeData.inputs.folderId
+ if (nodeData.inputs?.permissionId) defaultParams.permissionId = nodeData.inputs.permissionId
+ if (nodeData.inputs?.fileName) defaultParams.name = nodeData.inputs.fileName
+ if (nodeData.inputs?.fileContent) defaultParams.content = nodeData.inputs.fileContent
+ if (nodeData.inputs?.mimeType) defaultParams.mimeType = nodeData.inputs.mimeType
+ if (nodeData.inputs?.parentFolderId) defaultParams.parents = nodeData.inputs.parentFolderId
+ if (nodeData.inputs?.description) defaultParams.description = nodeData.inputs.description
+ if (nodeData.inputs?.searchQuery) defaultParams.query = nodeData.inputs.searchQuery
+ if (nodeData.inputs?.maxResults) defaultParams.pageSize = nodeData.inputs.maxResults
+ if (nodeData.inputs?.orderBy) defaultParams.orderBy = nodeData.inputs.orderBy
+ if (nodeData.inputs?.shareRole) defaultParams.role = nodeData.inputs.shareRole
+ if (nodeData.inputs?.shareType) defaultParams.type = nodeData.inputs.shareType
+ if (nodeData.inputs?.emailAddress) defaultParams.emailAddress = nodeData.inputs.emailAddress
+ if (nodeData.inputs?.domainName) defaultParams.domain = nodeData.inputs.domainName
+ if (nodeData.inputs?.sendNotificationEmail !== undefined)
+ defaultParams.sendNotificationEmail = nodeData.inputs.sendNotificationEmail
+ if (nodeData.inputs?.emailMessage) defaultParams.emailMessage = nodeData.inputs.emailMessage
+ if (nodeData.inputs?.includeItemsFromAllDrives !== undefined)
+ defaultParams.includeItemsFromAllDrives = nodeData.inputs.includeItemsFromAllDrives
+ if (nodeData.inputs?.supportsAllDrives !== undefined) defaultParams.supportsAllDrives = nodeData.inputs.supportsAllDrives
+ if (nodeData.inputs?.fields) defaultParams.fields = nodeData.inputs.fields
+ if (nodeData.inputs?.acknowledgeAbuse !== undefined) defaultParams.acknowledgeAbuse = nodeData.inputs.acknowledgeAbuse
+
+ return defaultParams
+ }
+}
+
+module.exports = { nodeClass: GoogleDrive_Tools }
diff --git a/packages/components/nodes/tools/GoogleDrive/core.ts b/packages/components/nodes/tools/GoogleDrive/core.ts
new file mode 100644
index 00000000000..62377f5dc02
--- /dev/null
+++ b/packages/components/nodes/tools/GoogleDrive/core.ts
@@ -0,0 +1,982 @@
+import { z } from 'zod'
+import fetch from 'node-fetch'
+import { DynamicStructuredTool } from '../OpenAPIToolkit/core'
+import { TOOL_ARGS_PREFIX } from '../../../src/agents'
+
+export const desc = `Use this when you want to access Google Drive API for managing files and folders`
+
+export interface Headers {
+ [key: string]: string
+}
+
+export interface Body {
+ [key: string]: any
+}
+
+export interface RequestParameters {
+ headers?: Headers
+ body?: Body
+ url?: string
+ description?: string
+ name?: string
+ actions?: string[]
+ accessToken?: string
+ defaultParams?: any
+}
+
+// Define schemas for different Google Drive operations
+
+// File Schemas
+const ListFilesSchema = z.object({
+ pageSize: z.number().optional().default(10).describe('Maximum number of files to return (1-1000)'),
+ pageToken: z.string().optional().describe('Token for next page of results'),
+ orderBy: z.string().optional().describe('Sort order (name, folder, createdTime, modifiedTime, etc.)'),
+ query: z.string().optional().describe('Search query (e.g., "name contains \'hello\'")'),
+ spaces: z.string().optional().default('drive').describe('Spaces to search (drive, appDataFolder, photos)'),
+ fields: z.string().optional().describe('Fields to include in response'),
+ includeItemsFromAllDrives: z.boolean().optional().describe('Include items from all drives'),
+ supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives')
+})
+
+const GetFileSchema = z.object({
+ fileId: z.string().describe('File ID'),
+ fields: z.string().optional().describe('Fields to include in response'),
+ supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives'),
+ acknowledgeAbuse: z
+ .boolean()
+ .optional()
+ .describe('Whether the user is acknowledging the risk of downloading known malware or other abusive files')
+})
+
+const CreateFileSchema = z.object({
+ name: z.string().describe('File name'),
+ parents: z.string().optional().describe('Comma-separated list of parent folder IDs'),
+ mimeType: z.string().optional().describe('MIME type of the file'),
+ description: z.string().optional().describe('File description'),
+ content: z.string().optional().describe('File content (for text files)'),
+ supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives')
+})
+
+const UpdateFileSchema = z.object({
+ fileId: z.string().describe('File ID to update'),
+ name: z.string().optional().describe('New file name'),
+ description: z.string().optional().describe('New file description'),
+ starred: z.boolean().optional().describe('Whether the file is starred'),
+ trashed: z.boolean().optional().describe('Whether the file is trashed'),
+ parents: z.string().optional().describe('Comma-separated list of new parent folder IDs'),
+ supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives')
+})
+
+const DeleteFileSchema = z.object({
+ fileId: z.string().describe('File ID to delete'),
+ supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives')
+})
+
+const CopyFileSchema = z.object({
+ fileId: z.string().describe('File ID to copy'),
+ name: z.string().describe('Name for the copied file'),
+ parents: z.string().optional().describe('Comma-separated list of parent folder IDs for the copy'),
+ supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives')
+})
+
+const DownloadFileSchema = z.object({
+ fileId: z.string().describe('File ID to download'),
+ acknowledgeAbuse: z
+ .boolean()
+ .optional()
+ .describe('Whether the user is acknowledging the risk of downloading known malware or other abusive files'),
+ supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives')
+})
+
+const CreateFolderSchema = z.object({
+ name: z.string().describe('Folder name'),
+ parents: z.string().optional().describe('Comma-separated list of parent folder IDs'),
+ description: z.string().optional().describe('Folder description'),
+ supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives')
+})
+
+const SearchFilesSchema = z.object({
+ query: z.string().describe('Search query using Google Drive search syntax'),
+ pageSize: z.number().optional().default(10).describe('Maximum number of files to return'),
+ orderBy: z.string().optional().describe('Sort order'),
+ includeItemsFromAllDrives: z.boolean().optional().describe('Include items from all drives'),
+ supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives')
+})
+
+const ShareFileSchema = z.object({
+ fileId: z.string().describe('File ID to share'),
+ role: z.enum(['reader', 'writer', 'commenter', 'owner']).describe('Permission role'),
+ type: z.enum(['user', 'group', 'domain', 'anyone']).describe('Permission type'),
+ emailAddress: z.string().optional().describe('Email address (required for user/group types)'),
+ domain: z.string().optional().describe('Domain name (required for domain type)'),
+ allowFileDiscovery: z.boolean().optional().describe('Whether the file can be discovered by search'),
+ sendNotificationEmail: z.boolean().optional().default(true).describe('Whether to send notification emails'),
+ emailMessage: z.string().optional().describe('Custom message to include in notification email'),
+ supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives')
+})
+
+class BaseGoogleDriveTool extends DynamicStructuredTool {
+ protected accessToken: string = ''
+
+ constructor(args: any) {
+ super(args)
+ this.accessToken = args.accessToken ?? ''
+ }
+
+ async makeGoogleDriveRequest({
+ endpoint,
+ method = 'GET',
+ body,
+ params
+ }: {
+ endpoint: string
+ method?: string
+ body?: any
+ params?: any
+ }): Promise {
+ const baseUrl = 'https://www.googleapis.com/drive/v3'
+ const url = `${baseUrl}/${endpoint}`
+
+ const headers: { [key: string]: string } = {
+ Authorization: `Bearer ${this.accessToken}`,
+ Accept: 'application/json',
+ ...this.headers
+ }
+
+ if (method !== 'GET' && body) {
+ headers['Content-Type'] = 'application/json'
+ }
+
+ const response = await fetch(url, {
+ method,
+ headers,
+ body: body ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ throw new Error(`Google Drive API Error ${response.status}: ${response.statusText} - ${errorText}`)
+ }
+
+ const data = await response.text()
+ return data + TOOL_ARGS_PREFIX + JSON.stringify(params)
+ }
+}
+
+// File Tools
+class ListFilesTool extends BaseGoogleDriveTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'list_files',
+ description: 'List files and folders from Google Drive',
+ schema: ListFilesSchema,
+ baseUrl: '',
+ method: 'GET',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const queryParams = new URLSearchParams()
+
+ if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString())
+ if (params.pageToken) queryParams.append('pageToken', params.pageToken)
+ if (params.orderBy) queryParams.append('orderBy', params.orderBy)
+ if (params.query) queryParams.append('q', params.query)
+ if (params.spaces) queryParams.append('spaces', params.spaces)
+ if (params.fields) queryParams.append('fields', params.fields)
+ if (params.includeItemsFromAllDrives) queryParams.append('includeItemsFromAllDrives', params.includeItemsFromAllDrives.toString())
+ if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
+
+ const endpoint = `files?${queryParams.toString()}`
+
+ try {
+ const response = await this.makeGoogleDriveRequest({ endpoint, params })
+ return response
+ } catch (error) {
+ return `Error listing files: ${error}`
+ }
+ }
+}
+
+class GetFileTool extends BaseGoogleDriveTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'get_file',
+ description: 'Get file metadata from Google Drive',
+ schema: GetFileSchema,
+ baseUrl: '',
+ method: 'GET',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const queryParams = new URLSearchParams()
+
+ if (params.fields) queryParams.append('fields', params.fields)
+ if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
+ if (params.acknowledgeAbuse) queryParams.append('acknowledgeAbuse', params.acknowledgeAbuse.toString())
+
+ const endpoint = `files/${encodeURIComponent(params.fileId)}?${queryParams.toString()}`
+
+ try {
+ const response = await this.makeGoogleDriveRequest({ endpoint, params })
+ return response
+ } catch (error) {
+ return `Error getting file: ${error}`
+ }
+ }
+}
+
+class CreateFileTool extends BaseGoogleDriveTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'create_file',
+ description: 'Create a new file in Google Drive',
+ schema: CreateFileSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ // Validate required parameters
+ if (!params.name) {
+ throw new Error('File name is required')
+ }
+
+ const queryParams = new URLSearchParams()
+ if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
+
+ // Prepare metadata
+ const fileMetadata: any = {
+ name: params.name
+ }
+
+ if (params.parents) {
+ // Validate parent folder IDs format
+ const parentIds = params.parents
+ .split(',')
+ .map((p: string) => p.trim())
+ .filter((p: string) => p.length > 0)
+ if (parentIds.length > 0) {
+ fileMetadata.parents = parentIds
+ }
+ }
+ if (params.mimeType) fileMetadata.mimeType = params.mimeType
+ if (params.description) fileMetadata.description = params.description
+
+ // Determine upload type based on content and metadata
+ if (!params.content) {
+ // Metadata-only upload (no file content) - standard endpoint
+ const endpoint = `files?${queryParams.toString()}`
+ const response = await this.makeGoogleDriveRequest({
+ endpoint,
+ method: 'POST',
+ body: fileMetadata,
+ params
+ })
+ return response
+ } else {
+ // Validate content
+ if (typeof params.content !== 'string') {
+ throw new Error('File content must be a string')
+ }
+
+ // Check if we have metadata beyond just the name
+ const hasAdditionalMetadata = params.parents || params.description || params.mimeType
+
+ if (!hasAdditionalMetadata) {
+ // Simple upload (uploadType=media) - only file content, basic metadata
+ return await this.performSimpleUpload(params, queryParams)
+ } else {
+ // Multipart upload (uploadType=multipart) - file content + metadata
+ return await this.performMultipartUpload(params, fileMetadata, queryParams)
+ }
+ }
+ } catch (error) {
+ return `Error creating file: ${error}`
+ }
+ }
+
+ private async performSimpleUpload(params: any, queryParams: URLSearchParams): Promise {
+ // Simple upload: POST https://www.googleapis.com/upload/drive/v3/files?uploadType=media
+ queryParams.append('uploadType', 'media')
+ const url = `https://www.googleapis.com/upload/drive/v3/files?${queryParams.toString()}`
+
+ const headers: { [key: string]: string } = {
+ Authorization: `Bearer ${this.accessToken}`,
+ 'Content-Type': params.mimeType || 'application/octet-stream',
+ 'Content-Length': Buffer.byteLength(params.content, 'utf8').toString()
+ }
+
+ const response = await fetch(url, {
+ method: 'POST',
+ headers,
+ body: params.content
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ throw new Error(`Google Drive API Error ${response.status}: ${response.statusText} - ${errorText}`)
+ }
+
+ const data = await response.text()
+ return data + TOOL_ARGS_PREFIX + JSON.stringify(params)
+ }
+
+ private async performMultipartUpload(params: any, fileMetadata: any, queryParams: URLSearchParams): Promise {
+ // Multipart upload: POST https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart
+ queryParams.append('uploadType', 'multipart')
+ const url = `https://www.googleapis.com/upload/drive/v3/files?${queryParams.toString()}`
+
+ // Create multipart/related body according to RFC 2387
+ const boundary = '-------314159265358979323846'
+
+ // Build multipart body - RFC 2387 format
+ let body = `--${boundary}\r\n`
+
+ // Part 1: Metadata (application/json; charset=UTF-8)
+ body += 'Content-Type: application/json; charset=UTF-8\r\n\r\n'
+ body += JSON.stringify(fileMetadata) + '\r\n'
+
+ // Part 2: Media content (any MIME type)
+ body += `--${boundary}\r\n`
+ body += `Content-Type: ${params.mimeType || 'application/octet-stream'}\r\n\r\n`
+ body += params.content + '\r\n'
+
+ // Close boundary
+ body += `--${boundary}--`
+
+ const headers: { [key: string]: string } = {
+ Authorization: `Bearer ${this.accessToken}`,
+ 'Content-Type': `multipart/related; boundary="${boundary}"`,
+ 'Content-Length': Buffer.byteLength(body, 'utf8').toString()
+ }
+
+ try {
+ const response = await fetch(url, {
+ method: 'POST',
+ headers,
+ body: body
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ console.error('Multipart upload failed:', {
+ url,
+ headers: { ...headers, Authorization: '[REDACTED]' },
+ metadata: fileMetadata,
+ contentLength: params.content?.length || 0,
+ error: errorText
+ })
+ throw new Error(`Google Drive API Error ${response.status}: ${response.statusText} - ${errorText}`)
+ }
+
+ const data = await response.text()
+ return data + TOOL_ARGS_PREFIX + JSON.stringify(params)
+ } catch (error) {
+ throw new Error(`Multipart upload failed: ${error}`)
+ }
+ }
+}
+
+class UpdateFileTool extends BaseGoogleDriveTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'update_file',
+ description: 'Update file metadata in Google Drive',
+ schema: UpdateFileSchema,
+ baseUrl: '',
+ method: 'PATCH',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const updateData: any = {}
+
+ if (params.name) updateData.name = params.name
+ if (params.description) updateData.description = params.description
+ if (params.starred !== undefined) updateData.starred = params.starred
+ if (params.trashed !== undefined) updateData.trashed = params.trashed
+
+ const queryParams = new URLSearchParams()
+ if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
+
+ const endpoint = `files/${encodeURIComponent(params.fileId)}?${queryParams.toString()}`
+
+ const response = await this.makeGoogleDriveRequest({
+ endpoint,
+ method: 'PATCH',
+ body: updateData,
+ params
+ })
+ return response
+ } catch (error) {
+ return `Error updating file: ${error}`
+ }
+ }
+}
+
+class DeleteFileTool extends BaseGoogleDriveTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'delete_file',
+ description: 'Delete a file from Google Drive',
+ schema: DeleteFileSchema,
+ baseUrl: '',
+ method: 'DELETE',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const queryParams = new URLSearchParams()
+ if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
+
+ const endpoint = `files/${encodeURIComponent(params.fileId)}?${queryParams.toString()}`
+
+ await this.makeGoogleDriveRequest({
+ endpoint,
+ method: 'DELETE',
+ params
+ })
+ return `File deleted successfully`
+ } catch (error) {
+ return `Error deleting file: ${error}`
+ }
+ }
+}
+
+class CopyFileTool extends BaseGoogleDriveTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'copy_file',
+ description: 'Copy a file in Google Drive',
+ schema: CopyFileSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const copyData: any = {
+ name: params.name
+ }
+
+ if (params.parents) {
+ copyData.parents = params.parents.split(',').map((p: string) => p.trim())
+ }
+
+ const queryParams = new URLSearchParams()
+ if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
+
+ const endpoint = `files/${encodeURIComponent(params.fileId)}/copy?${queryParams.toString()}`
+
+ const response = await this.makeGoogleDriveRequest({
+ endpoint,
+ method: 'POST',
+ body: copyData,
+ params
+ })
+ return response
+ } catch (error) {
+ return `Error copying file: ${error}`
+ }
+ }
+}
+
+class DownloadFileTool extends BaseGoogleDriveTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'download_file',
+ description: 'Download a file from Google Drive',
+ schema: DownloadFileSchema,
+ baseUrl: '',
+ method: 'GET',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const queryParams = new URLSearchParams()
+ queryParams.append('alt', 'media')
+ if (params.acknowledgeAbuse) queryParams.append('acknowledgeAbuse', params.acknowledgeAbuse.toString())
+ if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
+
+ const endpoint = `files/${encodeURIComponent(params.fileId)}?${queryParams.toString()}`
+
+ const response = await this.makeGoogleDriveRequest({ endpoint, params })
+ return response
+ } catch (error) {
+ return `Error downloading file: ${error}`
+ }
+ }
+}
+
+class CreateFolderTool extends BaseGoogleDriveTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'create_folder',
+ description: 'Create a new folder in Google Drive',
+ schema: CreateFolderSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const folderData: any = {
+ name: params.name,
+ mimeType: 'application/vnd.google-apps.folder'
+ }
+
+ if (params.parents) {
+ folderData.parents = params.parents.split(',').map((p: string) => p.trim())
+ }
+ if (params.description) folderData.description = params.description
+
+ const queryParams = new URLSearchParams()
+ if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
+
+ const endpoint = `files?${queryParams.toString()}`
+
+ const response = await this.makeGoogleDriveRequest({
+ endpoint,
+ method: 'POST',
+ body: folderData,
+ params
+ })
+ return response
+ } catch (error) {
+ return `Error creating folder: ${error}`
+ }
+ }
+}
+
+class SearchFilesTool extends BaseGoogleDriveTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'search_files',
+ description: 'Search files in Google Drive',
+ schema: SearchFilesSchema,
+ baseUrl: '',
+ method: 'GET',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const queryParams = new URLSearchParams()
+ queryParams.append('q', params.query)
+ if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString())
+ if (params.orderBy) queryParams.append('orderBy', params.orderBy)
+ if (params.includeItemsFromAllDrives)
+ queryParams.append('includeItemsFromAllDrives', params.includeItemsFromAllDrives.toString())
+ if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
+
+ const endpoint = `files?${queryParams.toString()}`
+
+ const response = await this.makeGoogleDriveRequest({ endpoint, params })
+ return response
+ } catch (error) {
+ return `Error searching files: ${error}`
+ }
+ }
+}
+
+class ShareFileTool extends BaseGoogleDriveTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'share_file',
+ description: 'Share a file in Google Drive',
+ schema: ShareFileSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const permissionData: any = {
+ role: params.role,
+ type: params.type
+ }
+
+ if (params.emailAddress) permissionData.emailAddress = params.emailAddress
+ if (params.domain) permissionData.domain = params.domain
+ if (params.allowFileDiscovery !== undefined) permissionData.allowFileDiscovery = params.allowFileDiscovery
+
+ const queryParams = new URLSearchParams()
+ if (params.sendNotificationEmail !== undefined)
+ queryParams.append('sendNotificationEmail', params.sendNotificationEmail.toString())
+ if (params.emailMessage) queryParams.append('emailMessage', params.emailMessage)
+ if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
+
+ const endpoint = `files/${encodeURIComponent(params.fileId)}/permissions?${queryParams.toString()}`
+
+ const response = await this.makeGoogleDriveRequest({
+ endpoint,
+ method: 'POST',
+ body: permissionData,
+ params
+ })
+ return response
+ } catch (error) {
+ return `Error sharing file: ${error}`
+ }
+ }
+}
+
+class ListFolderContentsTool extends BaseGoogleDriveTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'list_folder_contents',
+ description: 'List contents of a specific folder in Google Drive',
+ schema: z.object({
+ folderId: z.string().describe('Folder ID to list contents from'),
+ pageSize: z.number().optional().default(10).describe('Maximum number of files to return'),
+ orderBy: z.string().optional().describe('Sort order'),
+ includeItemsFromAllDrives: z.boolean().optional().describe('Include items from all drives'),
+ supportsAllDrives: z
+ .boolean()
+ .optional()
+ .describe('Whether the requesting application supports both My Drives and shared drives')
+ }),
+ baseUrl: '',
+ method: 'GET',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const queryParams = new URLSearchParams()
+ queryParams.append('q', `'${params.folderId}' in parents`)
+ if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString())
+ if (params.orderBy) queryParams.append('orderBy', params.orderBy)
+ if (params.includeItemsFromAllDrives)
+ queryParams.append('includeItemsFromAllDrives', params.includeItemsFromAllDrives.toString())
+ if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
+
+ const endpoint = `files?${queryParams.toString()}`
+
+ const response = await this.makeGoogleDriveRequest({ endpoint, params })
+ return response
+ } catch (error) {
+ return `Error listing folder contents: ${error}`
+ }
+ }
+}
+
+class DeleteFolderTool extends BaseGoogleDriveTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'delete_folder',
+ description: 'Delete a folder from Google Drive',
+ schema: z.object({
+ folderId: z.string().describe('Folder ID to delete'),
+ supportsAllDrives: z
+ .boolean()
+ .optional()
+ .describe('Whether the requesting application supports both My Drives and shared drives')
+ }),
+ baseUrl: '',
+ method: 'DELETE',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const queryParams = new URLSearchParams()
+ if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
+
+ const endpoint = `files/${encodeURIComponent(params.folderId)}?${queryParams.toString()}`
+
+ await this.makeGoogleDriveRequest({
+ endpoint,
+ method: 'DELETE',
+ params
+ })
+ return `Folder deleted successfully`
+ } catch (error) {
+ return `Error deleting folder: ${error}`
+ }
+ }
+}
+
+class GetPermissionsTool extends BaseGoogleDriveTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'get_permissions',
+ description: 'Get permissions for a file in Google Drive',
+ schema: z.object({
+ fileId: z.string().describe('File ID to get permissions for'),
+ supportsAllDrives: z
+ .boolean()
+ .optional()
+ .describe('Whether the requesting application supports both My Drives and shared drives')
+ }),
+ baseUrl: '',
+ method: 'GET',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const queryParams = new URLSearchParams()
+ if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
+
+ const endpoint = `files/${encodeURIComponent(params.fileId)}/permissions?${queryParams.toString()}`
+
+ const response = await this.makeGoogleDriveRequest({ endpoint, params })
+ return response
+ } catch (error) {
+ return `Error getting permissions: ${error}`
+ }
+ }
+}
+
+class RemovePermissionTool extends BaseGoogleDriveTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'remove_permission',
+ description: 'Remove a permission from a file in Google Drive',
+ schema: z.object({
+ fileId: z.string().describe('File ID to remove permission from'),
+ permissionId: z.string().describe('Permission ID to remove'),
+ supportsAllDrives: z
+ .boolean()
+ .optional()
+ .describe('Whether the requesting application supports both My Drives and shared drives')
+ }),
+ baseUrl: '',
+ method: 'DELETE',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const queryParams = new URLSearchParams()
+ if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
+
+ const endpoint = `files/${encodeURIComponent(params.fileId)}/permissions/${encodeURIComponent(
+ params.permissionId
+ )}?${queryParams.toString()}`
+
+ await this.makeGoogleDriveRequest({
+ endpoint,
+ method: 'DELETE',
+ params
+ })
+ return `Permission removed successfully`
+ } catch (error) {
+ return `Error removing permission: ${error}`
+ }
+ }
+}
+
+export const createGoogleDriveTools = (args?: RequestParameters): DynamicStructuredTool[] => {
+ const tools: DynamicStructuredTool[] = []
+ const actions = args?.actions || []
+ const accessToken = args?.accessToken || ''
+ const defaultParams = args?.defaultParams || {}
+
+ if (actions.includes('listFiles')) {
+ tools.push(new ListFilesTool({ accessToken, defaultParams }))
+ }
+
+ if (actions.includes('getFile')) {
+ tools.push(new GetFileTool({ accessToken, defaultParams }))
+ }
+
+ if (actions.includes('createFile')) {
+ tools.push(new CreateFileTool({ accessToken, defaultParams }))
+ }
+
+ if (actions.includes('updateFile')) {
+ tools.push(new UpdateFileTool({ accessToken, defaultParams }))
+ }
+
+ if (actions.includes('deleteFile')) {
+ tools.push(new DeleteFileTool({ accessToken, defaultParams }))
+ }
+
+ if (actions.includes('copyFile')) {
+ tools.push(new CopyFileTool({ accessToken, defaultParams }))
+ }
+
+ if (actions.includes('downloadFile')) {
+ tools.push(new DownloadFileTool({ accessToken, defaultParams }))
+ }
+
+ if (actions.includes('createFolder')) {
+ tools.push(new CreateFolderTool({ accessToken, defaultParams }))
+ }
+
+ if (actions.includes('listFolderContents')) {
+ tools.push(new ListFolderContentsTool({ accessToken, defaultParams }))
+ }
+
+ if (actions.includes('deleteFolder')) {
+ tools.push(new DeleteFolderTool({ accessToken, defaultParams }))
+ }
+
+ if (actions.includes('searchFiles')) {
+ tools.push(new SearchFilesTool({ accessToken, defaultParams }))
+ }
+
+ if (actions.includes('shareFile')) {
+ tools.push(new ShareFileTool({ accessToken, defaultParams }))
+ }
+
+ if (actions.includes('getPermissions')) {
+ tools.push(new GetPermissionsTool({ accessToken, defaultParams }))
+ }
+
+ if (actions.includes('removePermission')) {
+ tools.push(new RemovePermissionTool({ accessToken, defaultParams }))
+ }
+
+ return tools
+}
diff --git a/packages/components/nodes/tools/GoogleDrive/google-drive.svg b/packages/components/nodes/tools/GoogleDrive/google-drive.svg
new file mode 100644
index 00000000000..03b2f21290a
--- /dev/null
+++ b/packages/components/nodes/tools/GoogleDrive/google-drive.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/components/nodes/tools/GoogleSheets/GoogleSheets.ts b/packages/components/nodes/tools/GoogleSheets/GoogleSheets.ts
new file mode 100644
index 00000000000..60f15903ac7
--- /dev/null
+++ b/packages/components/nodes/tools/GoogleSheets/GoogleSheets.ts
@@ -0,0 +1,368 @@
+import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils'
+import { createGoogleSheetsTools } from './core'
+import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
+
+class GoogleSheets_Tools implements INode {
+ label: string
+ name: string
+ version: number
+ type: string
+ icon: string
+ category: string
+ description: string
+ baseClasses: string[]
+ credential: INodeParams
+ inputs: INodeParams[]
+
+ constructor() {
+ this.label = 'Google Sheets'
+ this.name = 'googleSheetsTool'
+ this.version = 1.0
+ this.type = 'GoogleSheets'
+ this.icon = 'google-sheets.svg'
+ this.category = 'Tools'
+ this.description = 'Perform Google Sheets operations such as managing spreadsheets, reading and writing values'
+ this.baseClasses = ['Tool']
+ this.credential = {
+ label: 'Connect Credential',
+ name: 'credential',
+ type: 'credential',
+ credentialNames: ['googleSheetsOAuth2']
+ }
+ this.inputs = [
+ {
+ label: 'Type',
+ name: 'sheetsType',
+ type: 'options',
+ description: 'Type of Google Sheets operation',
+ options: [
+ {
+ label: 'Spreadsheet',
+ name: 'spreadsheet'
+ },
+ {
+ label: 'Values',
+ name: 'values'
+ }
+ ]
+ },
+ // Spreadsheet Actions
+ {
+ label: 'Spreadsheet Actions',
+ name: 'spreadsheetActions',
+ type: 'multiOptions',
+ description: 'Actions to perform on spreadsheets',
+ options: [
+ {
+ label: 'Create Spreadsheet',
+ name: 'createSpreadsheet'
+ },
+ {
+ label: 'Get Spreadsheet',
+ name: 'getSpreadsheet'
+ },
+ {
+ label: 'Update Spreadsheet',
+ name: 'updateSpreadsheet'
+ }
+ ],
+ show: {
+ sheetsType: ['spreadsheet']
+ }
+ },
+ // Values Actions
+ {
+ label: 'Values Actions',
+ name: 'valuesActions',
+ type: 'multiOptions',
+ description: 'Actions to perform on sheet values',
+ options: [
+ {
+ label: 'Get Values',
+ name: 'getValues'
+ },
+ {
+ label: 'Update Values',
+ name: 'updateValues'
+ },
+ {
+ label: 'Append Values',
+ name: 'appendValues'
+ },
+ {
+ label: 'Clear Values',
+ name: 'clearValues'
+ },
+ {
+ label: 'Batch Get Values',
+ name: 'batchGetValues'
+ },
+ {
+ label: 'Batch Update Values',
+ name: 'batchUpdateValues'
+ },
+ {
+ label: 'Batch Clear Values',
+ name: 'batchClearValues'
+ }
+ ],
+ show: {
+ sheetsType: ['values']
+ }
+ },
+ // Spreadsheet Parameters
+ {
+ label: 'Spreadsheet ID',
+ name: 'spreadsheetId',
+ type: 'string',
+ description: 'The ID of the spreadsheet',
+ show: {
+ sheetsType: ['spreadsheet', 'values']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Title',
+ name: 'title',
+ type: 'string',
+ description: 'The title of the spreadsheet',
+ show: {
+ spreadsheetActions: ['createSpreadsheet', 'updateSpreadsheet']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Sheet Count',
+ name: 'sheetCount',
+ type: 'number',
+ description: 'Number of sheets to create',
+ default: 1,
+ show: {
+ spreadsheetActions: ['createSpreadsheet']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Values Parameters
+ {
+ label: 'Range',
+ name: 'range',
+ type: 'string',
+ description: 'The range to read/write (e.g., A1:B2, Sheet1!A1:C10)',
+ show: {
+ valuesActions: ['getValues', 'updateValues', 'clearValues']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Ranges',
+ name: 'ranges',
+ type: 'string',
+ description: 'Comma-separated list of ranges for batch operations',
+ show: {
+ valuesActions: ['batchGetValues', 'batchClearValues']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Values',
+ name: 'values',
+ type: 'string',
+ description: 'JSON array of values to write (e.g., [["A1", "B1"], ["A2", "B2"]])',
+ show: {
+ valuesActions: ['updateValues', 'appendValues', 'batchUpdateValues']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Value Input Option',
+ name: 'valueInputOption',
+ type: 'options',
+ description: 'How input data should be interpreted',
+ options: [
+ {
+ label: 'Raw',
+ name: 'RAW'
+ },
+ {
+ label: 'User Entered',
+ name: 'USER_ENTERED'
+ }
+ ],
+ default: 'USER_ENTERED',
+ show: {
+ valuesActions: ['updateValues', 'appendValues', 'batchUpdateValues']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Value Render Option',
+ name: 'valueRenderOption',
+ type: 'options',
+ description: 'How values should be represented in the output',
+ options: [
+ {
+ label: 'Formatted Value',
+ name: 'FORMATTED_VALUE'
+ },
+ {
+ label: 'Unformatted Value',
+ name: 'UNFORMATTED_VALUE'
+ },
+ {
+ label: 'Formula',
+ name: 'FORMULA'
+ }
+ ],
+ default: 'FORMATTED_VALUE',
+ show: {
+ valuesActions: ['getValues', 'batchGetValues']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Date Time Render Option',
+ name: 'dateTimeRenderOption',
+ type: 'options',
+ description: 'How dates, times, and durations should be represented',
+ options: [
+ {
+ label: 'Serial Number',
+ name: 'SERIAL_NUMBER'
+ },
+ {
+ label: 'Formatted String',
+ name: 'FORMATTED_STRING'
+ }
+ ],
+ default: 'FORMATTED_STRING',
+ show: {
+ valuesActions: ['getValues', 'batchGetValues']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Insert Data Option',
+ name: 'insertDataOption',
+ type: 'options',
+ description: 'How data should be inserted',
+ options: [
+ {
+ label: 'Overwrite',
+ name: 'OVERWRITE'
+ },
+ {
+ label: 'Insert Rows',
+ name: 'INSERT_ROWS'
+ }
+ ],
+ default: 'OVERWRITE',
+ show: {
+ valuesActions: ['appendValues']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Include Grid Data',
+ name: 'includeGridData',
+ type: 'boolean',
+ description: 'True if grid data should be returned',
+ default: false,
+ show: {
+ spreadsheetActions: ['getSpreadsheet']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Major Dimension',
+ name: 'majorDimension',
+ type: 'options',
+ description: 'The major dimension that results should use',
+ options: [
+ {
+ label: 'Rows',
+ name: 'ROWS'
+ },
+ {
+ label: 'Columns',
+ name: 'COLUMNS'
+ }
+ ],
+ default: 'ROWS',
+ show: {
+ valuesActions: ['getValues', 'updateValues', 'appendValues', 'batchGetValues', 'batchUpdateValues']
+ },
+ additionalParams: true,
+ optional: true
+ }
+ ]
+ }
+
+ async init(nodeData: INodeData, _: string, options: ICommonObject): Promise {
+ const sheetsType = nodeData.inputs?.sheetsType as string
+
+ let credentialData = await getCredentialData(nodeData.credential ?? '', options)
+ credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options)
+ const accessToken = getCredentialParam('access_token', credentialData, nodeData)
+
+ if (!accessToken) {
+ throw new Error('No access token found in credential')
+ }
+
+ // Get all actions based on type
+ let actions: string[] = []
+
+ if (sheetsType === 'spreadsheet') {
+ actions = convertMultiOptionsToStringArray(nodeData.inputs?.spreadsheetActions)
+ } else if (sheetsType === 'values') {
+ actions = convertMultiOptionsToStringArray(nodeData.inputs?.valuesActions)
+ }
+
+ const defaultParams = this.transformNodeInputsToToolArgs(nodeData)
+
+ const tools = createGoogleSheetsTools({
+ accessToken,
+ actions,
+ defaultParams
+ })
+
+ return tools
+ }
+
+ transformNodeInputsToToolArgs(nodeData: INodeData): Record {
+ // Collect default parameters from inputs
+ const defaultParams: Record = {}
+
+ // Common parameters
+ if (nodeData.inputs?.spreadsheetId) defaultParams.spreadsheetId = nodeData.inputs.spreadsheetId
+
+ // Spreadsheet parameters
+ if (nodeData.inputs?.title) defaultParams.title = nodeData.inputs.title
+ if (nodeData.inputs?.sheetCount) defaultParams.sheetCount = nodeData.inputs.sheetCount
+ if (nodeData.inputs?.includeGridData !== undefined) defaultParams.includeGridData = nodeData.inputs.includeGridData
+
+ // Values parameters
+ if (nodeData.inputs?.range) defaultParams.range = nodeData.inputs.range
+ if (nodeData.inputs?.ranges) defaultParams.ranges = nodeData.inputs.ranges
+ if (nodeData.inputs?.values) defaultParams.values = nodeData.inputs.values
+ if (nodeData.inputs?.valueInputOption) defaultParams.valueInputOption = nodeData.inputs.valueInputOption
+ if (nodeData.inputs?.valueRenderOption) defaultParams.valueRenderOption = nodeData.inputs.valueRenderOption
+ if (nodeData.inputs?.dateTimeRenderOption) defaultParams.dateTimeRenderOption = nodeData.inputs.dateTimeRenderOption
+ if (nodeData.inputs?.insertDataOption) defaultParams.insertDataOption = nodeData.inputs.insertDataOption
+ if (nodeData.inputs?.majorDimension) defaultParams.majorDimension = nodeData.inputs.majorDimension
+
+ return defaultParams
+ }
+}
+
+module.exports = { nodeClass: GoogleSheets_Tools }
diff --git a/packages/components/nodes/tools/GoogleSheets/core.ts b/packages/components/nodes/tools/GoogleSheets/core.ts
new file mode 100644
index 00000000000..8b6359844b7
--- /dev/null
+++ b/packages/components/nodes/tools/GoogleSheets/core.ts
@@ -0,0 +1,631 @@
+import { z } from 'zod'
+import fetch from 'node-fetch'
+import { DynamicStructuredTool } from '../OpenAPIToolkit/core'
+import { TOOL_ARGS_PREFIX } from '../../../src/agents'
+
+export const desc = `Use this when you want to access Google Sheets API for managing spreadsheets and values`
+
+export interface Headers {
+ [key: string]: string
+}
+
+export interface Body {
+ [key: string]: any
+}
+
+export interface RequestParameters {
+ headers?: Headers
+ body?: Body
+ url?: string
+ description?: string
+ name?: string
+ actions?: string[]
+ accessToken?: string
+ defaultParams?: any
+}
+
+// Define schemas for different Google Sheets operations
+
+// Spreadsheet Schemas
+const CreateSpreadsheetSchema = z.object({
+ title: z.string().describe('The title of the spreadsheet'),
+ sheetCount: z.number().optional().default(1).describe('Number of sheets to create'),
+ locale: z.string().optional().describe('The locale of the spreadsheet (e.g., en_US)'),
+ timeZone: z.string().optional().describe('The time zone of the spreadsheet (e.g., America/New_York)')
+})
+
+const GetSpreadsheetSchema = z.object({
+ spreadsheetId: z.string().describe('The ID of the spreadsheet to retrieve'),
+ ranges: z.string().optional().describe('Comma-separated list of ranges to retrieve'),
+ includeGridData: z.boolean().optional().default(false).describe('True if grid data should be returned')
+})
+
+const UpdateSpreadsheetSchema = z.object({
+ spreadsheetId: z.string().describe('The ID of the spreadsheet to update'),
+ title: z.string().optional().describe('New title for the spreadsheet'),
+ locale: z.string().optional().describe('New locale for the spreadsheet'),
+ timeZone: z.string().optional().describe('New time zone for the spreadsheet')
+})
+
+// Values Schemas
+const GetValuesSchema = z.object({
+ spreadsheetId: z.string().describe('The ID of the spreadsheet'),
+ range: z.string().describe('The A1 notation of the range to retrieve values from'),
+ valueRenderOption: z
+ .enum(['FORMATTED_VALUE', 'UNFORMATTED_VALUE', 'FORMULA'])
+ .optional()
+ .default('FORMATTED_VALUE')
+ .describe('How values should be represented'),
+ dateTimeRenderOption: z
+ .enum(['SERIAL_NUMBER', 'FORMATTED_STRING'])
+ .optional()
+ .default('FORMATTED_STRING')
+ .describe('How dates should be represented'),
+ majorDimension: z.enum(['ROWS', 'COLUMNS']).optional().default('ROWS').describe('The major dimension that results should use')
+})
+
+const UpdateValuesSchema = z.object({
+ spreadsheetId: z.string().describe('The ID of the spreadsheet'),
+ range: z.string().describe('The A1 notation of the range to update'),
+ values: z.string().describe('JSON array of values to write (e.g., [["A1", "B1"], ["A2", "B2"]])'),
+ valueInputOption: z.enum(['RAW', 'USER_ENTERED']).optional().default('USER_ENTERED').describe('How input data should be interpreted'),
+ majorDimension: z.enum(['ROWS', 'COLUMNS']).optional().default('ROWS').describe('The major dimension of the values')
+})
+
+const AppendValuesSchema = z.object({
+ spreadsheetId: z.string().describe('The ID of the spreadsheet'),
+ range: z.string().describe('The A1 notation of the range to append to'),
+ values: z.string().describe('JSON array of values to append'),
+ valueInputOption: z.enum(['RAW', 'USER_ENTERED']).optional().default('USER_ENTERED').describe('How input data should be interpreted'),
+ insertDataOption: z.enum(['OVERWRITE', 'INSERT_ROWS']).optional().default('OVERWRITE').describe('How data should be inserted'),
+ majorDimension: z.enum(['ROWS', 'COLUMNS']).optional().default('ROWS').describe('The major dimension of the values')
+})
+
+const ClearValuesSchema = z.object({
+ spreadsheetId: z.string().describe('The ID of the spreadsheet'),
+ range: z.string().describe('The A1 notation of the range to clear')
+})
+
+const BatchGetValuesSchema = z.object({
+ spreadsheetId: z.string().describe('The ID of the spreadsheet'),
+ ranges: z.string().describe('Comma-separated list of ranges to retrieve'),
+ valueRenderOption: z
+ .enum(['FORMATTED_VALUE', 'UNFORMATTED_VALUE', 'FORMULA'])
+ .optional()
+ .default('FORMATTED_VALUE')
+ .describe('How values should be represented'),
+ dateTimeRenderOption: z
+ .enum(['SERIAL_NUMBER', 'FORMATTED_STRING'])
+ .optional()
+ .default('FORMATTED_STRING')
+ .describe('How dates should be represented'),
+ majorDimension: z.enum(['ROWS', 'COLUMNS']).optional().default('ROWS').describe('The major dimension that results should use')
+})
+
+const BatchUpdateValuesSchema = z.object({
+ spreadsheetId: z.string().describe('The ID of the spreadsheet'),
+ valueInputOption: z.enum(['RAW', 'USER_ENTERED']).optional().default('USER_ENTERED').describe('How input data should be interpreted'),
+ values: z
+ .string()
+ .describe('JSON array of value ranges to update (e.g., [{"range": "A1:B2", "values": [["A1", "B1"], ["A2", "B2"]]}])'),
+ includeValuesInResponse: z.boolean().optional().default(false).describe('Whether to return the updated values in the response')
+})
+
+const BatchClearValuesSchema = z.object({
+ spreadsheetId: z.string().describe('The ID of the spreadsheet'),
+ ranges: z.string().describe('Comma-separated list of ranges to clear')
+})
+
+class BaseGoogleSheetsTool extends DynamicStructuredTool {
+ protected accessToken: string = ''
+
+ constructor(args: any) {
+ super(args)
+ this.accessToken = args.accessToken ?? ''
+ }
+
+ async makeGoogleSheetsRequest({
+ endpoint,
+ method = 'GET',
+ body,
+ params
+ }: {
+ endpoint: string
+ method?: string
+ body?: any
+ params?: any
+ }): Promise {
+ const url = `https://sheets.googleapis.com/v4/${endpoint}`
+
+ const headers = {
+ Authorization: `Bearer ${this.accessToken}`,
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ ...this.headers
+ }
+
+ const response = await fetch(url, {
+ method,
+ headers,
+ body: body ? JSON.stringify(body) : undefined
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ throw new Error(`Google Sheets API Error ${response.status}: ${response.statusText} - ${errorText}`)
+ }
+
+ const data = await response.text()
+ return data + TOOL_ARGS_PREFIX + JSON.stringify(params)
+ }
+}
+
+// Spreadsheet Tools
+class CreateSpreadsheetTool extends BaseGoogleSheetsTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'create_spreadsheet',
+ description: 'Create a new Google Spreadsheet',
+ schema: CreateSpreadsheetSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ const body: any = {
+ properties: {
+ title: params.title
+ }
+ }
+
+ if (params.locale) body.properties.locale = params.locale
+ if (params.timeZone) body.properties.timeZone = params.timeZone
+
+ // Add sheets if specified
+ if (params.sheetCount && params.sheetCount > 1) {
+ body.sheets = []
+ for (let i = 0; i < params.sheetCount; i++) {
+ body.sheets.push({
+ properties: {
+ title: i === 0 ? 'Sheet1' : `Sheet${i + 1}`
+ }
+ })
+ }
+ }
+
+ return await this.makeGoogleSheetsRequest({
+ endpoint: 'spreadsheets',
+ method: 'POST',
+ body,
+ params
+ })
+ }
+}
+
+class GetSpreadsheetTool extends BaseGoogleSheetsTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'get_spreadsheet',
+ description: 'Get a Google Spreadsheet by ID',
+ schema: GetSpreadsheetSchema,
+ baseUrl: '',
+ method: 'GET',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const queryParams = new URLSearchParams()
+
+ if (params.ranges) {
+ params.ranges.split(',').forEach((range: string) => {
+ queryParams.append('ranges', range.trim())
+ })
+ }
+ if (params.includeGridData) queryParams.append('includeGridData', 'true')
+
+ const queryString = queryParams.toString()
+ const endpoint = `spreadsheets/${params.spreadsheetId}${queryString ? `?${queryString}` : ''}`
+
+ return await this.makeGoogleSheetsRequest({
+ endpoint,
+ method: 'GET',
+ params
+ })
+ }
+}
+
+class UpdateSpreadsheetTool extends BaseGoogleSheetsTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'update_spreadsheet',
+ description: 'Update a Google Spreadsheet properties',
+ schema: UpdateSpreadsheetSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ const requests = []
+ if (params.title || params.locale || params.timeZone) {
+ const updateProperties: any = {}
+ if (params.title) updateProperties.title = params.title
+ if (params.locale) updateProperties.locale = params.locale
+ if (params.timeZone) updateProperties.timeZone = params.timeZone
+
+ requests.push({
+ updateSpreadsheetProperties: {
+ properties: updateProperties,
+ fields: Object.keys(updateProperties).join(',')
+ }
+ })
+ }
+
+ const body = { requests }
+
+ return await this.makeGoogleSheetsRequest({
+ endpoint: `spreadsheets/${params.spreadsheetId}:batchUpdate`,
+ method: 'POST',
+ body,
+ params
+ })
+ }
+}
+
+// Values Tools
+class GetValuesTool extends BaseGoogleSheetsTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'get_values',
+ description: 'Get values from a Google Spreadsheet range',
+ schema: GetValuesSchema,
+ baseUrl: '',
+ method: 'GET',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const queryParams = new URLSearchParams()
+
+ if (params.valueRenderOption) queryParams.append('valueRenderOption', params.valueRenderOption)
+ if (params.dateTimeRenderOption) queryParams.append('dateTimeRenderOption', params.dateTimeRenderOption)
+ if (params.majorDimension) queryParams.append('majorDimension', params.majorDimension)
+
+ const queryString = queryParams.toString()
+ const encodedRange = encodeURIComponent(params.range)
+ const endpoint = `spreadsheets/${params.spreadsheetId}/values/${encodedRange}${queryString ? `?${queryString}` : ''}`
+
+ return await this.makeGoogleSheetsRequest({
+ endpoint,
+ method: 'GET',
+ params
+ })
+ }
+}
+
+class UpdateValuesTool extends BaseGoogleSheetsTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'update_values',
+ description: 'Update values in a Google Spreadsheet range',
+ schema: UpdateValuesSchema,
+ baseUrl: '',
+ method: 'PUT',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ let values
+ try {
+ values = JSON.parse(params.values)
+ } catch (error) {
+ throw new Error('Values must be a valid JSON array')
+ }
+
+ const body = {
+ values,
+ majorDimension: params.majorDimension || 'ROWS'
+ }
+
+ const queryParams = new URLSearchParams()
+ queryParams.append('valueInputOption', params.valueInputOption || 'USER_ENTERED')
+
+ const encodedRange = encodeURIComponent(params.range)
+ const endpoint = `spreadsheets/${params.spreadsheetId}/values/${encodedRange}?${queryParams.toString()}`
+
+ return await this.makeGoogleSheetsRequest({
+ endpoint,
+ method: 'PUT',
+ body,
+ params
+ })
+ }
+}
+
+class AppendValuesTool extends BaseGoogleSheetsTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'append_values',
+ description: 'Append values to a Google Spreadsheet range',
+ schema: AppendValuesSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ let values
+ try {
+ values = JSON.parse(params.values)
+ } catch (error) {
+ throw new Error('Values must be a valid JSON array')
+ }
+
+ const body = {
+ values,
+ majorDimension: params.majorDimension || 'ROWS'
+ }
+
+ const queryParams = new URLSearchParams()
+ queryParams.append('valueInputOption', params.valueInputOption || 'USER_ENTERED')
+ queryParams.append('insertDataOption', params.insertDataOption || 'OVERWRITE')
+
+ const encodedRange = encodeURIComponent(params.range)
+ const endpoint = `spreadsheets/${params.spreadsheetId}/values/${encodedRange}:append?${queryParams.toString()}`
+
+ return await this.makeGoogleSheetsRequest({
+ endpoint,
+ method: 'POST',
+ body,
+ params
+ })
+ }
+}
+
+class ClearValuesTool extends BaseGoogleSheetsTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'clear_values',
+ description: 'Clear values from a Google Spreadsheet range',
+ schema: ClearValuesSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ const encodedRange = encodeURIComponent(params.range)
+ const endpoint = `spreadsheets/${params.spreadsheetId}/values/${encodedRange}:clear`
+
+ return await this.makeGoogleSheetsRequest({
+ endpoint,
+ method: 'POST',
+ body: {},
+ params
+ })
+ }
+}
+
+class BatchGetValuesTool extends BaseGoogleSheetsTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'batch_get_values',
+ description: 'Get values from multiple Google Spreadsheet ranges',
+ schema: BatchGetValuesSchema,
+ baseUrl: '',
+ method: 'GET',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const queryParams = new URLSearchParams()
+
+ // Add ranges
+ params.ranges.split(',').forEach((range: string) => {
+ queryParams.append('ranges', range.trim())
+ })
+
+ if (params.valueRenderOption) queryParams.append('valueRenderOption', params.valueRenderOption)
+ if (params.dateTimeRenderOption) queryParams.append('dateTimeRenderOption', params.dateTimeRenderOption)
+ if (params.majorDimension) queryParams.append('majorDimension', params.majorDimension)
+
+ const endpoint = `spreadsheets/${params.spreadsheetId}/values:batchGet?${queryParams.toString()}`
+
+ return await this.makeGoogleSheetsRequest({
+ endpoint,
+ method: 'GET',
+ params
+ })
+ }
+}
+
+class BatchUpdateValuesTool extends BaseGoogleSheetsTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'batch_update_values',
+ description: 'Update values in multiple Google Spreadsheet ranges',
+ schema: BatchUpdateValuesSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ let valueRanges
+ try {
+ valueRanges = JSON.parse(params.values)
+ } catch (error) {
+ throw new Error('Values must be a valid JSON array of value ranges')
+ }
+
+ const body = {
+ valueInputOption: params.valueInputOption || 'USER_ENTERED',
+ data: valueRanges,
+ includeValuesInResponse: params.includeValuesInResponse || false
+ }
+
+ const endpoint = `spreadsheets/${params.spreadsheetId}/values:batchUpdate`
+
+ return await this.makeGoogleSheetsRequest({
+ endpoint,
+ method: 'POST',
+ body,
+ params
+ })
+ }
+}
+
+class BatchClearValuesTool extends BaseGoogleSheetsTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'batch_clear_values',
+ description: 'Clear values from multiple Google Spreadsheet ranges',
+ schema: BatchClearValuesSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ accessToken: args.accessToken
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ const ranges = params.ranges.split(',').map((range: string) => range.trim())
+ const body = { ranges }
+
+ const endpoint = `spreadsheets/${params.spreadsheetId}/values:batchClear`
+
+ return await this.makeGoogleSheetsRequest({
+ endpoint,
+ method: 'POST',
+ body,
+ params
+ })
+ }
+}
+
+export const createGoogleSheetsTools = (args?: RequestParameters): DynamicStructuredTool[] => {
+ const { actions = [], accessToken, defaultParams } = args || {}
+ const tools: DynamicStructuredTool[] = []
+
+ // Define all available tools
+ const toolClasses = {
+ // Spreadsheet tools
+ createSpreadsheet: CreateSpreadsheetTool,
+ getSpreadsheet: GetSpreadsheetTool,
+ updateSpreadsheet: UpdateSpreadsheetTool,
+ // Values tools
+ getValues: GetValuesTool,
+ updateValues: UpdateValuesTool,
+ appendValues: AppendValuesTool,
+ clearValues: ClearValuesTool,
+ batchGetValues: BatchGetValuesTool,
+ batchUpdateValues: BatchUpdateValuesTool,
+ batchClearValues: BatchClearValuesTool
+ }
+
+ // Create tools based on requested actions
+ actions.forEach((action) => {
+ const ToolClass = toolClasses[action as keyof typeof toolClasses]
+ if (ToolClass) {
+ tools.push(new ToolClass({ accessToken, defaultParams }))
+ }
+ })
+
+ return tools
+}
diff --git a/packages/components/nodes/tools/GoogleSheets/google-sheets.svg b/packages/components/nodes/tools/GoogleSheets/google-sheets.svg
new file mode 100644
index 00000000000..43af0ccf1fe
--- /dev/null
+++ b/packages/components/nodes/tools/GoogleSheets/google-sheets.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/components/nodes/tools/Jira/Jira.ts b/packages/components/nodes/tools/Jira/Jira.ts
new file mode 100644
index 00000000000..95c2b8c045b
--- /dev/null
+++ b/packages/components/nodes/tools/Jira/Jira.ts
@@ -0,0 +1,449 @@
+import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam } from '../../../src/utils'
+import { createJiraTools } from './core'
+import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
+
+class Jira_Tools implements INode {
+ label: string
+ name: string
+ version: number
+ type: string
+ icon: string
+ category: string
+ description: string
+ baseClasses: string[]
+ credential: INodeParams
+ inputs: INodeParams[]
+
+ constructor() {
+ this.label = 'Jira'
+ this.name = 'jiraTool'
+ this.version = 1.0
+ this.type = 'Jira'
+ this.icon = 'jira.svg'
+ this.category = 'Tools'
+ this.description = 'Perform Jira operations for issues, comments, and users'
+ this.baseClasses = [this.type, 'Tool']
+ this.credential = {
+ label: 'Connect Credential',
+ name: 'credential',
+ type: 'credential',
+ credentialNames: ['jiraApi']
+ }
+ this.inputs = [
+ {
+ label: 'Host',
+ name: 'jiraHost',
+ type: 'string',
+ placeholder: 'https://example.atlassian.net'
+ },
+ {
+ label: 'Type',
+ name: 'jiraType',
+ type: 'options',
+ options: [
+ {
+ label: 'Issues',
+ name: 'issues'
+ },
+ {
+ label: 'Issue Comments',
+ name: 'comments'
+ },
+ {
+ label: 'Users',
+ name: 'users'
+ }
+ ]
+ },
+ // Issue Actions
+ {
+ label: 'Issue Actions',
+ name: 'issueActions',
+ type: 'multiOptions',
+ options: [
+ {
+ label: 'List Issues',
+ name: 'listIssues'
+ },
+ {
+ label: 'Create Issue',
+ name: 'createIssue'
+ },
+ {
+ label: 'Get Issue',
+ name: 'getIssue'
+ },
+ {
+ label: 'Update Issue',
+ name: 'updateIssue'
+ },
+ {
+ label: 'Delete Issue',
+ name: 'deleteIssue'
+ },
+ {
+ label: 'Assign Issue',
+ name: 'assignIssue'
+ },
+ {
+ label: 'Transition Issue',
+ name: 'transitionIssue'
+ }
+ ],
+ show: {
+ jiraType: ['issues']
+ }
+ },
+ // Comment Actions
+ {
+ label: 'Comment Actions',
+ name: 'commentActions',
+ type: 'multiOptions',
+ options: [
+ {
+ label: 'List Comments',
+ name: 'listComments'
+ },
+ {
+ label: 'Create Comment',
+ name: 'createComment'
+ },
+ {
+ label: 'Get Comment',
+ name: 'getComment'
+ },
+ {
+ label: 'Update Comment',
+ name: 'updateComment'
+ },
+ {
+ label: 'Delete Comment',
+ name: 'deleteComment'
+ }
+ ],
+ show: {
+ jiraType: ['comments']
+ }
+ },
+ // User Actions
+ {
+ label: 'User Actions',
+ name: 'userActions',
+ type: 'multiOptions',
+ options: [
+ {
+ label: 'Search Users',
+ name: 'searchUsers'
+ },
+ {
+ label: 'Get User',
+ name: 'getUser'
+ },
+ {
+ label: 'Create User',
+ name: 'createUser'
+ },
+ {
+ label: 'Update User',
+ name: 'updateUser'
+ },
+ {
+ label: 'Delete User',
+ name: 'deleteUser'
+ }
+ ],
+ show: {
+ jiraType: ['users']
+ }
+ },
+ // ISSUE PARAMETERS
+ {
+ label: 'Project Key',
+ name: 'projectKey',
+ type: 'string',
+ placeholder: 'PROJ',
+ description: 'Project key for the issue',
+ show: {
+ issueActions: ['listIssues', 'createIssue']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Issue Type',
+ name: 'issueType',
+ type: 'string',
+ placeholder: 'Bug, Task, Story',
+ description: 'Type of issue to create',
+ show: {
+ issueActions: ['createIssue']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Summary',
+ name: 'issueSummary',
+ type: 'string',
+ description: 'Issue summary/title',
+ show: {
+ issueActions: ['createIssue', 'updateIssue']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Description',
+ name: 'issueDescription',
+ type: 'string',
+ description: 'Issue description',
+ show: {
+ issueActions: ['createIssue', 'updateIssue']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Priority',
+ name: 'issuePriority',
+ type: 'string',
+ placeholder: 'Highest, High, Medium, Low, Lowest',
+ description: 'Issue priority',
+ show: {
+ issueActions: ['createIssue', 'updateIssue']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Issue Key',
+ name: 'issueKey',
+ type: 'string',
+ placeholder: 'PROJ-123',
+ description: 'Issue key (e.g., PROJ-123)',
+ show: {
+ issueActions: ['getIssue', 'updateIssue', 'deleteIssue', 'assignIssue', 'transitionIssue']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Assignee Account ID',
+ name: 'assigneeAccountId',
+ type: 'string',
+ description: 'Account ID of the user to assign',
+ show: {
+ issueActions: ['assignIssue', 'createIssue', 'updateIssue']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Transition ID',
+ name: 'transitionId',
+ type: 'string',
+ description: 'ID of the transition to execute',
+ show: {
+ issueActions: ['transitionIssue']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'JQL Query',
+ name: 'jqlQuery',
+ type: 'string',
+ placeholder: 'project = PROJ AND status = "To Do"',
+ description: 'JQL query for filtering issues',
+ show: {
+ issueActions: ['listIssues']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Max Results',
+ name: 'issueMaxResults',
+ type: 'number',
+ default: 50,
+ description: 'Maximum number of issues to return',
+ show: {
+ issueActions: ['listIssues']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // COMMENT PARAMETERS
+ {
+ label: 'Issue Key (for Comments)',
+ name: 'commentIssueKey',
+ type: 'string',
+ placeholder: 'PROJ-123',
+ description: 'Issue key for comment operations',
+ show: {
+ commentActions: ['listComments', 'createComment']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Comment Text',
+ name: 'commentText',
+ type: 'string',
+ description: 'Comment content',
+ show: {
+ commentActions: ['createComment', 'updateComment']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Comment ID',
+ name: 'commentId',
+ type: 'string',
+ description: 'ID of the comment',
+ show: {
+ commentActions: ['getComment', 'updateComment', 'deleteComment']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // USER PARAMETERS
+ {
+ label: 'Search Query',
+ name: 'userQuery',
+ type: 'string',
+ placeholder: 'john.doe',
+ description: 'Query string for user search',
+ show: {
+ userActions: ['searchUsers']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Account ID',
+ name: 'userAccountId',
+ type: 'string',
+ description: 'User account ID',
+ show: {
+ userActions: ['getUser', 'updateUser', 'deleteUser']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Email Address',
+ name: 'userEmail',
+ type: 'string',
+ placeholder: 'user@example.com',
+ description: 'User email address',
+ show: {
+ userActions: ['createUser', 'updateUser']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Display Name',
+ name: 'userDisplayName',
+ type: 'string',
+ description: 'User display name',
+ show: {
+ userActions: ['createUser', 'updateUser']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'User Max Results',
+ name: 'userMaxResults',
+ type: 'number',
+ default: 50,
+ description: 'Maximum number of users to return',
+ show: {
+ userActions: ['searchUsers']
+ },
+ additionalParams: true,
+ optional: true
+ }
+ ]
+ }
+
+ async init(nodeData: INodeData, _: string, options: ICommonObject): Promise {
+ let credentialData = await getCredentialData(nodeData.credential ?? '', options)
+ const username = getCredentialParam('username', credentialData, nodeData)
+ const accessToken = getCredentialParam('accessToken', credentialData, nodeData)
+ const jiraHost = nodeData.inputs?.jiraHost as string
+
+ if (!username) {
+ throw new Error('No username found in credential')
+ }
+
+ if (!accessToken) {
+ throw new Error('No access token found in credential')
+ }
+
+ if (!jiraHost) {
+ throw new Error('No Jira host provided')
+ }
+
+ // Get all actions based on type
+ const jiraType = nodeData.inputs?.jiraType as string
+ let actions: string[] = []
+
+ if (jiraType === 'issues') {
+ actions = convertMultiOptionsToStringArray(nodeData.inputs?.issueActions)
+ } else if (jiraType === 'comments') {
+ actions = convertMultiOptionsToStringArray(nodeData.inputs?.commentActions)
+ } else if (jiraType === 'users') {
+ actions = convertMultiOptionsToStringArray(nodeData.inputs?.userActions)
+ }
+
+ const defaultParams = this.transformNodeInputsToToolArgs(nodeData)
+
+ // Create and return tools based on selected actions
+ const tools = createJiraTools({
+ actions,
+ username,
+ accessToken,
+ jiraHost,
+ defaultParams
+ })
+
+ return tools
+ }
+
+ transformNodeInputsToToolArgs(nodeData: INodeData): Record {
+ // Collect default parameters from inputs
+ const defaultParams: Record = {}
+
+ // Issue parameters
+ if (nodeData.inputs?.projectKey) defaultParams.projectKey = nodeData.inputs.projectKey
+ if (nodeData.inputs?.issueType) defaultParams.issueType = nodeData.inputs.issueType
+ if (nodeData.inputs?.issueSummary) defaultParams.issueSummary = nodeData.inputs.issueSummary
+ if (nodeData.inputs?.issueDescription) defaultParams.issueDescription = nodeData.inputs.issueDescription
+ if (nodeData.inputs?.issuePriority) defaultParams.issuePriority = nodeData.inputs.issuePriority
+ if (nodeData.inputs?.issueKey) defaultParams.issueKey = nodeData.inputs.issueKey
+ if (nodeData.inputs?.assigneeAccountId) defaultParams.assigneeAccountId = nodeData.inputs.assigneeAccountId
+ if (nodeData.inputs?.transitionId) defaultParams.transitionId = nodeData.inputs.transitionId
+ if (nodeData.inputs?.jqlQuery) defaultParams.jqlQuery = nodeData.inputs.jqlQuery
+ if (nodeData.inputs?.issueMaxResults) defaultParams.issueMaxResults = nodeData.inputs.issueMaxResults
+
+ // Comment parameters
+ if (nodeData.inputs?.commentIssueKey) defaultParams.commentIssueKey = nodeData.inputs.commentIssueKey
+ if (nodeData.inputs?.commentText) defaultParams.commentText = nodeData.inputs.commentText
+ if (nodeData.inputs?.commentId) defaultParams.commentId = nodeData.inputs.commentId
+
+ // User parameters
+ if (nodeData.inputs?.userQuery) defaultParams.userQuery = nodeData.inputs.userQuery
+ if (nodeData.inputs?.userAccountId) defaultParams.userAccountId = nodeData.inputs.userAccountId
+ if (nodeData.inputs?.userEmail) defaultParams.userEmail = nodeData.inputs.userEmail
+ if (nodeData.inputs?.userDisplayName) defaultParams.userDisplayName = nodeData.inputs.userDisplayName
+ if (nodeData.inputs?.userMaxResults) defaultParams.userMaxResults = nodeData.inputs.userMaxResults
+
+ return defaultParams
+ }
+}
+
+module.exports = { nodeClass: Jira_Tools }
diff --git a/packages/components/nodes/tools/Jira/core.ts b/packages/components/nodes/tools/Jira/core.ts
new file mode 100644
index 00000000000..09d73e0ca53
--- /dev/null
+++ b/packages/components/nodes/tools/Jira/core.ts
@@ -0,0 +1,1172 @@
+import { z } from 'zod'
+import fetch from 'node-fetch'
+import { DynamicStructuredTool } from '../OpenAPIToolkit/core'
+import { TOOL_ARGS_PREFIX } from '../../../src/agents'
+
+export const desc = `Use this when you want to access Jira API for managing issues, comments, and users`
+
+export interface Headers {
+ [key: string]: string
+}
+
+export interface Body {
+ [key: string]: any
+}
+
+export interface RequestParameters {
+ headers?: Headers
+ body?: Body
+ url?: string
+ description?: string
+ maxOutputLength?: number
+ name?: string
+ actions?: string[]
+ username?: string
+ accessToken?: string
+ jiraHost?: string
+ defaultParams?: any
+}
+
+// Define schemas for different Jira operations
+
+// Issue Schemas
+const ListIssuesSchema = z.object({
+ projectKey: z.string().optional().describe('Project key to filter issues'),
+ jql: z.string().optional().describe('JQL query for filtering issues'),
+ maxResults: z.number().optional().default(50).describe('Maximum number of results to return'),
+ startAt: z.number().optional().default(0).describe('Index of the first result to return')
+})
+
+const CreateIssueSchema = z.object({
+ projectKey: z.string().describe('Project key where the issue will be created'),
+ issueType: z.string().describe('Type of issue (Bug, Task, Story, etc.)'),
+ summary: z.string().describe('Issue summary/title'),
+ description: z.string().optional().describe('Issue description'),
+ priority: z.string().optional().describe('Issue priority (Highest, High, Medium, Low, Lowest)'),
+ assigneeAccountId: z.string().optional().describe('Account ID of the assignee'),
+ labels: z.array(z.string()).optional().describe('Labels to add to the issue')
+})
+
+const GetIssueSchema = z.object({
+ issueKey: z.string().describe('Issue key (e.g., PROJ-123)')
+})
+
+const UpdateIssueSchema = z.object({
+ issueKey: z.string().describe('Issue key (e.g., PROJ-123)'),
+ summary: z.string().optional().describe('Updated issue summary/title'),
+ description: z.string().optional().describe('Updated issue description'),
+ priority: z.string().optional().describe('Updated issue priority'),
+ assigneeAccountId: z.string().optional().describe('Account ID of the new assignee')
+})
+
+const AssignIssueSchema = z.object({
+ issueKey: z.string().describe('Issue key (e.g., PROJ-123)'),
+ assigneeAccountId: z.string().describe('Account ID of the user to assign')
+})
+
+const TransitionIssueSchema = z.object({
+ issueKey: z.string().describe('Issue key (e.g., PROJ-123)'),
+ transitionId: z.string().describe('ID of the transition to execute')
+})
+
+// Comment Schemas
+const ListCommentsSchema = z.object({
+ issueKey: z.string().describe('Issue key to get comments for'),
+ maxResults: z.number().optional().default(50).describe('Maximum number of results to return'),
+ startAt: z.number().optional().default(0).describe('Index of the first result to return')
+})
+
+const CreateCommentSchema = z.object({
+ issueKey: z.string().describe('Issue key to add comment to'),
+ text: z.string().describe('Comment text content'),
+ visibility: z
+ .object({
+ type: z.string().optional(),
+ value: z.string().optional()
+ })
+ .optional()
+ .describe('Comment visibility settings')
+})
+
+const GetCommentSchema = z.object({
+ issueKey: z.string().describe('Issue key'),
+ commentId: z.string().describe('Comment ID')
+})
+
+const UpdateCommentSchema = z.object({
+ issueKey: z.string().describe('Issue key'),
+ commentId: z.string().describe('Comment ID'),
+ text: z.string().describe('Updated comment text')
+})
+
+const DeleteCommentSchema = z.object({
+ issueKey: z.string().describe('Issue key'),
+ commentId: z.string().describe('Comment ID to delete')
+})
+
+// User Schemas
+const SearchUsersSchema = z.object({
+ query: z.string().describe('Query string for user search'),
+ maxResults: z.number().optional().default(50).describe('Maximum number of results to return'),
+ startAt: z.number().optional().default(0).describe('Index of the first result to return')
+})
+
+const GetUserSchema = z.object({
+ accountId: z.string().describe('Account ID of the user')
+})
+
+const CreateUserSchema = z.object({
+ emailAddress: z.string().describe('Email address of the user'),
+ displayName: z.string().describe('Display name of the user'),
+ username: z.string().optional().describe('Username (deprecated in newer versions)')
+})
+
+const UpdateUserSchema = z.object({
+ accountId: z.string().describe('Account ID of the user'),
+ emailAddress: z.string().optional().describe('Updated email address'),
+ displayName: z.string().optional().describe('Updated display name')
+})
+
+const DeleteUserSchema = z.object({
+ accountId: z.string().describe('Account ID of the user to delete')
+})
+
+class BaseJiraTool extends DynamicStructuredTool {
+ protected username: string = ''
+ protected accessToken: string = ''
+ protected jiraHost: string = ''
+
+ constructor(args: any) {
+ super(args)
+ this.username = args.username ?? ''
+ this.accessToken = args.accessToken ?? ''
+ this.jiraHost = args.jiraHost ?? ''
+ }
+
+ async makeJiraRequest({
+ endpoint,
+ method = 'GET',
+ body,
+ params
+ }: {
+ endpoint: string
+ method?: string
+ body?: any
+ params?: any
+ }): Promise {
+ const url = `${this.jiraHost}/rest/api/3/${endpoint}`
+ const auth = Buffer.from(`${this.username}:${this.accessToken}`).toString('base64')
+
+ const headers = {
+ Authorization: `Basic ${auth}`,
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ ...this.headers
+ }
+
+ const response = await fetch(url, {
+ method,
+ headers,
+ body: body ? JSON.stringify(body) : undefined
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ throw new Error(`Jira API Error ${response.status}: ${response.statusText} - ${errorText}`)
+ }
+
+ const data = await response.text()
+ return data + TOOL_ARGS_PREFIX + JSON.stringify(params)
+ }
+}
+
+// Issue Tools
+class ListIssuesTool extends BaseJiraTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'list_issues',
+ description: 'List issues from Jira using JQL query',
+ schema: ListIssuesSchema,
+ baseUrl: '',
+ method: 'GET',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ username: args.username,
+ accessToken: args.accessToken,
+ jiraHost: args.jiraHost,
+ maxOutputLength: args.maxOutputLength
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const queryParams = new URLSearchParams()
+
+ let jql = params.jql || ''
+ if (params.projectKey && !jql.includes('project')) {
+ jql = jql ? `project = ${params.projectKey} AND (${jql})` : `project = ${params.projectKey}`
+ }
+
+ if (jql) queryParams.append('jql', jql)
+ if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString())
+ if (params.startAt) queryParams.append('startAt', params.startAt.toString())
+
+ const endpoint = `search?${queryParams.toString()}`
+
+ try {
+ const response = await this.makeJiraRequest({ endpoint, params })
+ return response
+ } catch (error) {
+ return `Error listing issues: ${error}`
+ }
+ }
+}
+
+class CreateIssueTool extends BaseJiraTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'create_issue',
+ description: 'Create a new issue in Jira',
+ schema: CreateIssueSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ username: args.username,
+ accessToken: args.accessToken,
+ jiraHost: args.jiraHost,
+ maxOutputLength: args.maxOutputLength
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const issueData: any = {
+ fields: {
+ project: {
+ key: params.projectKey
+ },
+ issuetype: {
+ name: params.issueType
+ },
+ summary: params.summary
+ }
+ }
+
+ if (params.description) {
+ issueData.fields.description = {
+ type: 'doc',
+ version: 1,
+ content: [
+ {
+ type: 'paragraph',
+ content: [
+ {
+ type: 'text',
+ text: params.description
+ }
+ ]
+ }
+ ]
+ }
+ }
+
+ if (params.priority) {
+ issueData.fields.priority = {
+ name: params.priority
+ }
+ }
+
+ if (params.assigneeAccountId) {
+ issueData.fields.assignee = {
+ accountId: params.assigneeAccountId
+ }
+ }
+
+ if (params.labels) {
+ issueData.fields.labels = params.labels
+ }
+
+ const response = await this.makeJiraRequest({ endpoint: 'issue', method: 'POST', body: issueData, params })
+ return response
+ } catch (error) {
+ return `Error creating issue: ${error}`
+ }
+ }
+}
+
+class GetIssueTool extends BaseJiraTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'get_issue',
+ description: 'Get a specific issue from Jira',
+ schema: GetIssueSchema,
+ baseUrl: '',
+ method: 'GET',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ username: args.username,
+ accessToken: args.accessToken,
+ jiraHost: args.jiraHost,
+ maxOutputLength: args.maxOutputLength
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const endpoint = `issue/${params.issueKey}`
+ const response = await this.makeJiraRequest({ endpoint, params })
+ return response
+ } catch (error) {
+ return `Error getting issue: ${error}`
+ }
+ }
+}
+
+class UpdateIssueTool extends BaseJiraTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'update_issue',
+ description: 'Update an existing issue in Jira',
+ schema: UpdateIssueSchema,
+ baseUrl: '',
+ method: 'PUT',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ username: args.username,
+ accessToken: args.accessToken,
+ jiraHost: args.jiraHost,
+ maxOutputLength: args.maxOutputLength
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const updateData: any = {
+ fields: {}
+ }
+
+ if (params.summary) updateData.fields.summary = params.summary
+ if (params.description) {
+ updateData.fields.description = {
+ type: 'doc',
+ version: 1,
+ content: [
+ {
+ type: 'paragraph',
+ content: [
+ {
+ type: 'text',
+ text: params.description
+ }
+ ]
+ }
+ ]
+ }
+ }
+ if (params.priority) {
+ updateData.fields.priority = {
+ name: params.priority
+ }
+ }
+ if (params.assigneeAccountId) {
+ updateData.fields.assignee = {
+ accountId: params.assigneeAccountId
+ }
+ }
+
+ const endpoint = `issue/${params.issueKey}`
+ const response = await this.makeJiraRequest({ endpoint, method: 'PUT', body: updateData, params })
+ return response || 'Issue updated successfully'
+ } catch (error) {
+ return `Error updating issue: ${error}`
+ }
+ }
+}
+
+class DeleteIssueTool extends BaseJiraTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'delete_issue',
+ description: 'Delete an issue from Jira',
+ schema: GetIssueSchema,
+ baseUrl: '',
+ method: 'DELETE',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ username: args.username,
+ accessToken: args.accessToken,
+ jiraHost: args.jiraHost,
+ maxOutputLength: args.maxOutputLength
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const endpoint = `issue/${params.issueKey}`
+ const response = await this.makeJiraRequest({ endpoint, method: 'DELETE', params })
+ return response || 'Issue deleted successfully'
+ } catch (error) {
+ return `Error deleting issue: ${error}`
+ }
+ }
+}
+
+class AssignIssueTool extends BaseJiraTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'assign_issue',
+ description: 'Assign an issue to a user in Jira',
+ schema: AssignIssueSchema,
+ baseUrl: '',
+ method: 'PUT',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ username: args.username,
+ accessToken: args.accessToken,
+ jiraHost: args.jiraHost,
+ maxOutputLength: args.maxOutputLength
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const assignData = {
+ accountId: params.assigneeAccountId
+ }
+
+ const endpoint = `issue/${params.issueKey}/assignee`
+ const response = await this.makeJiraRequest({ endpoint, method: 'PUT', body: assignData, params })
+ return response || 'Issue assigned successfully'
+ } catch (error) {
+ return `Error assigning issue: ${error}`
+ }
+ }
+}
+
+class TransitionIssueTool extends BaseJiraTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'transition_issue',
+ description: 'Transition an issue to a different status in Jira',
+ schema: TransitionIssueSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ username: args.username,
+ accessToken: args.accessToken,
+ jiraHost: args.jiraHost,
+ maxOutputLength: args.maxOutputLength
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const transitionData = {
+ transition: {
+ id: params.transitionId
+ }
+ }
+
+ const endpoint = `issue/${params.issueKey}/transitions`
+ const response = await this.makeJiraRequest({ endpoint, method: 'POST', body: transitionData, params })
+ return response || 'Issue transitioned successfully'
+ } catch (error) {
+ return `Error transitioning issue: ${error}`
+ }
+ }
+}
+
+// Comment Tools
+class ListCommentsTool extends BaseJiraTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'list_comments',
+ description: 'List comments for a Jira issue',
+ schema: ListCommentsSchema,
+ baseUrl: '',
+ method: 'GET',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ username: args.username,
+ accessToken: args.accessToken,
+ jiraHost: args.jiraHost,
+ maxOutputLength: args.maxOutputLength
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const queryParams = new URLSearchParams()
+
+ if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString())
+ if (params.startAt) queryParams.append('startAt', params.startAt.toString())
+
+ const endpoint = `issue/${params.issueKey}/comment?${queryParams.toString()}`
+
+ try {
+ const response = await this.makeJiraRequest({ endpoint, params })
+ return response
+ } catch (error) {
+ return `Error listing comments: ${error}`
+ }
+ }
+}
+
+class CreateCommentTool extends BaseJiraTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'create_comment',
+ description: 'Create a comment on a Jira issue',
+ schema: CreateCommentSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ username: args.username,
+ accessToken: args.accessToken,
+ jiraHost: args.jiraHost,
+ maxOutputLength: args.maxOutputLength
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const commentData: any = {
+ body: {
+ type: 'doc',
+ version: 1,
+ content: [
+ {
+ type: 'paragraph',
+ content: [
+ {
+ type: 'text',
+ text: params.text
+ }
+ ]
+ }
+ ]
+ }
+ }
+
+ if (params.visibility) {
+ commentData.visibility = params.visibility
+ }
+
+ const endpoint = `issue/${params.issueKey}/comment`
+ const response = await this.makeJiraRequest({ endpoint, method: 'POST', body: commentData, params })
+ return response
+ } catch (error) {
+ return `Error creating comment: ${error}`
+ }
+ }
+}
+
+class GetCommentTool extends BaseJiraTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'get_comment',
+ description: 'Get a specific comment from a Jira issue',
+ schema: GetCommentSchema,
+ baseUrl: '',
+ method: 'GET',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ username: args.username,
+ accessToken: args.accessToken,
+ jiraHost: args.jiraHost,
+ maxOutputLength: args.maxOutputLength
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const endpoint = `issue/${params.issueKey}/comment/${params.commentId}`
+ const response = await this.makeJiraRequest({ endpoint, params })
+ return response
+ } catch (error) {
+ return `Error getting comment: ${error}`
+ }
+ }
+}
+
+class UpdateCommentTool extends BaseJiraTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'update_comment',
+ description: 'Update a comment on a Jira issue',
+ schema: UpdateCommentSchema,
+ baseUrl: '',
+ method: 'PUT',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ username: args.username,
+ accessToken: args.accessToken,
+ jiraHost: args.jiraHost,
+ maxOutputLength: args.maxOutputLength
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const commentData = {
+ body: {
+ type: 'doc',
+ version: 1,
+ content: [
+ {
+ type: 'paragraph',
+ content: [
+ {
+ type: 'text',
+ text: params.text
+ }
+ ]
+ }
+ ]
+ }
+ }
+
+ const endpoint = `issue/${params.issueKey}/comment/${params.commentId}`
+ const response = await this.makeJiraRequest({ endpoint, method: 'PUT', body: commentData, params })
+ return response || 'Comment updated successfully'
+ } catch (error) {
+ return `Error updating comment: ${error}`
+ }
+ }
+}
+
+class DeleteCommentTool extends BaseJiraTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'delete_comment',
+ description: 'Delete a comment from a Jira issue',
+ schema: DeleteCommentSchema,
+ baseUrl: '',
+ method: 'DELETE',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ username: args.username,
+ accessToken: args.accessToken,
+ jiraHost: args.jiraHost,
+ maxOutputLength: args.maxOutputLength
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const endpoint = `issue/${params.issueKey}/comment/${params.commentId}`
+ const response = await this.makeJiraRequest({ endpoint, method: 'DELETE', params })
+ return response || 'Comment deleted successfully'
+ } catch (error) {
+ return `Error deleting comment: ${error}`
+ }
+ }
+}
+
+// User Tools
+class SearchUsersTool extends BaseJiraTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'search_users',
+ description: 'Search for users in Jira',
+ schema: SearchUsersSchema,
+ baseUrl: '',
+ method: 'GET',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ username: args.username,
+ accessToken: args.accessToken,
+ jiraHost: args.jiraHost,
+ maxOutputLength: args.maxOutputLength
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const queryParams = new URLSearchParams()
+
+ if (params.query) queryParams.append('query', params.query)
+ if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString())
+ if (params.startAt) queryParams.append('startAt', params.startAt.toString())
+
+ const endpoint = `user/search?${queryParams.toString()}`
+
+ try {
+ const response = await this.makeJiraRequest({ endpoint, params })
+ return response
+ } catch (error) {
+ return `Error searching users: ${error}`
+ }
+ }
+}
+
+class GetUserTool extends BaseJiraTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'get_user',
+ description: 'Get a specific user from Jira',
+ schema: GetUserSchema,
+ baseUrl: '',
+ method: 'GET',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ username: args.username,
+ accessToken: args.accessToken,
+ jiraHost: args.jiraHost,
+ maxOutputLength: args.maxOutputLength
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const queryParams = new URLSearchParams()
+
+ queryParams.append('accountId', params.accountId)
+
+ const endpoint = `user?${queryParams.toString()}`
+
+ try {
+ const response = await this.makeJiraRequest({ endpoint, params })
+ return response
+ } catch (error) {
+ return `Error getting user: ${error}`
+ }
+ }
+}
+
+class CreateUserTool extends BaseJiraTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'create_user',
+ description: 'Create a new user in Jira',
+ schema: CreateUserSchema,
+ baseUrl: '',
+ method: 'POST',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ username: args.username,
+ accessToken: args.accessToken,
+ jiraHost: args.jiraHost,
+ maxOutputLength: args.maxOutputLength
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const userData: any = {
+ emailAddress: params.emailAddress,
+ displayName: params.displayName
+ }
+
+ if (params.username) {
+ userData.username = params.username
+ }
+
+ const endpoint = 'user'
+ const response = await this.makeJiraRequest({ endpoint, method: 'POST', body: userData, params })
+ return response
+ } catch (error) {
+ return `Error creating user: ${error}`
+ }
+ }
+}
+
+class UpdateUserTool extends BaseJiraTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'update_user',
+ description: 'Update an existing user in Jira',
+ schema: UpdateUserSchema,
+ baseUrl: '',
+ method: 'PUT',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ username: args.username,
+ accessToken: args.accessToken,
+ jiraHost: args.jiraHost,
+ maxOutputLength: args.maxOutputLength
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const userData: any = {}
+
+ if (params.emailAddress) userData.emailAddress = params.emailAddress
+ if (params.displayName) userData.displayName = params.displayName
+
+ const queryParams = new URLSearchParams()
+ queryParams.append('accountId', params.accountId)
+
+ const endpoint = `user?${queryParams.toString()}`
+ const response = await this.makeJiraRequest({ endpoint, method: 'PUT', body: userData, params })
+ return response || 'User updated successfully'
+ } catch (error) {
+ return `Error updating user: ${error}`
+ }
+ }
+}
+
+class DeleteUserTool extends BaseJiraTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'delete_user',
+ description: 'Delete a user from Jira',
+ schema: DeleteUserSchema,
+ baseUrl: '',
+ method: 'DELETE',
+ headers: {}
+ }
+ super({
+ ...toolInput,
+ username: args.username,
+ accessToken: args.accessToken,
+ jiraHost: args.jiraHost,
+ maxOutputLength: args.maxOutputLength
+ })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const queryParams = new URLSearchParams()
+ queryParams.append('accountId', params.accountId)
+
+ const endpoint = `user?${queryParams.toString()}`
+ const response = await this.makeJiraRequest({ endpoint, method: 'DELETE', params })
+ return response || 'User deleted successfully'
+ } catch (error) {
+ return `Error deleting user: ${error}`
+ }
+ }
+}
+
+export const createJiraTools = (args?: RequestParameters): DynamicStructuredTool[] => {
+ const tools: DynamicStructuredTool[] = []
+ const actions = args?.actions || []
+ const username = args?.username || ''
+ const accessToken = args?.accessToken || ''
+ const jiraHost = args?.jiraHost || ''
+ const maxOutputLength = args?.maxOutputLength || Infinity
+ const defaultParams = args?.defaultParams || {}
+
+ // Issue tools
+ if (actions.includes('listIssues')) {
+ tools.push(
+ new ListIssuesTool({
+ username,
+ accessToken,
+ jiraHost,
+ maxOutputLength,
+ defaultParams: defaultParams.listIssues
+ })
+ )
+ }
+
+ if (actions.includes('createIssue')) {
+ tools.push(
+ new CreateIssueTool({
+ username,
+ accessToken,
+ jiraHost,
+ maxOutputLength,
+ defaultParams: defaultParams.createIssue
+ })
+ )
+ }
+
+ if (actions.includes('getIssue')) {
+ tools.push(
+ new GetIssueTool({
+ username,
+ accessToken,
+ jiraHost,
+ maxOutputLength,
+ defaultParams: defaultParams.getIssue
+ })
+ )
+ }
+
+ if (actions.includes('updateIssue')) {
+ tools.push(
+ new UpdateIssueTool({
+ username,
+ accessToken,
+ jiraHost,
+ maxOutputLength,
+ defaultParams: defaultParams.updateIssue
+ })
+ )
+ }
+
+ if (actions.includes('deleteIssue')) {
+ tools.push(
+ new DeleteIssueTool({
+ username,
+ accessToken,
+ jiraHost,
+ maxOutputLength,
+ defaultParams: defaultParams.deleteIssue
+ })
+ )
+ }
+
+ if (actions.includes('assignIssue')) {
+ tools.push(
+ new AssignIssueTool({
+ username,
+ accessToken,
+ jiraHost,
+ maxOutputLength,
+ defaultParams: defaultParams.assignIssue
+ })
+ )
+ }
+
+ if (actions.includes('transitionIssue')) {
+ tools.push(
+ new TransitionIssueTool({
+ username,
+ accessToken,
+ jiraHost,
+ maxOutputLength,
+ defaultParams: defaultParams.transitionIssue
+ })
+ )
+ }
+
+ // Comment tools
+ if (actions.includes('listComments')) {
+ tools.push(
+ new ListCommentsTool({
+ username,
+ accessToken,
+ jiraHost,
+ maxOutputLength,
+ defaultParams: defaultParams.listComments
+ })
+ )
+ }
+
+ if (actions.includes('createComment')) {
+ tools.push(
+ new CreateCommentTool({
+ username,
+ accessToken,
+ jiraHost,
+ maxOutputLength,
+ defaultParams: defaultParams.createComment
+ })
+ )
+ }
+
+ if (actions.includes('getComment')) {
+ tools.push(
+ new GetCommentTool({
+ username,
+ accessToken,
+ jiraHost,
+ maxOutputLength,
+ defaultParams: defaultParams.getComment
+ })
+ )
+ }
+
+ if (actions.includes('updateComment')) {
+ tools.push(
+ new UpdateCommentTool({
+ username,
+ accessToken,
+ jiraHost,
+ maxOutputLength,
+ defaultParams: defaultParams.updateComment
+ })
+ )
+ }
+
+ if (actions.includes('deleteComment')) {
+ tools.push(
+ new DeleteCommentTool({
+ username,
+ accessToken,
+ jiraHost,
+ maxOutputLength,
+ defaultParams: defaultParams.deleteComment
+ })
+ )
+ }
+
+ // User tools
+ if (actions.includes('searchUsers')) {
+ tools.push(
+ new SearchUsersTool({
+ username,
+ accessToken,
+ jiraHost,
+ maxOutputLength,
+ defaultParams: defaultParams.searchUsers
+ })
+ )
+ }
+
+ if (actions.includes('getUser')) {
+ tools.push(
+ new GetUserTool({
+ username,
+ accessToken,
+ jiraHost,
+ maxOutputLength,
+ defaultParams: defaultParams.getUser
+ })
+ )
+ }
+
+ if (actions.includes('createUser')) {
+ tools.push(
+ new CreateUserTool({
+ username,
+ accessToken,
+ jiraHost,
+ maxOutputLength,
+ defaultParams: defaultParams.createUser
+ })
+ )
+ }
+
+ if (actions.includes('updateUser')) {
+ tools.push(
+ new UpdateUserTool({
+ username,
+ accessToken,
+ jiraHost,
+ maxOutputLength,
+ defaultParams: defaultParams.updateUser
+ })
+ )
+ }
+
+ if (actions.includes('deleteUser')) {
+ tools.push(
+ new DeleteUserTool({
+ username,
+ accessToken,
+ jiraHost,
+ maxOutputLength,
+ defaultParams: defaultParams.deleteUser
+ })
+ )
+ }
+
+ return tools
+}
diff --git a/packages/components/nodes/tools/Jira/jira.svg b/packages/components/nodes/tools/Jira/jira.svg
new file mode 100644
index 00000000000..4ace5cc84a3
--- /dev/null
+++ b/packages/components/nodes/tools/Jira/jira.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts b/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts
index c782a357cf8..d81c5d778f9 100644
--- a/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts
+++ b/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts
@@ -27,6 +27,16 @@ For example, you have a variable called "var1":
}
}
\`\`\`
+
+For example, when using SSE, you can use the variable "var1" in the headers:
+\`\`\`json
+{
+ "url": "https://api.example.com/endpoint/sse",
+ "headers": {
+ "Authorization": "Bearer {{$vars.var1}}"
+ }
+}
+\`\`\`
`
class Custom_MCP implements INode {
diff --git a/packages/components/nodes/tools/MCP/core.ts b/packages/components/nodes/tools/MCP/core.ts
index 7c894fcc479..9ac0ab1ae19 100644
--- a/packages/components/nodes/tools/MCP/core.ts
+++ b/packages/components/nodes/tools/MCP/core.ts
@@ -53,10 +53,29 @@ export class MCPToolkit extends BaseToolkit {
const baseUrl = new URL(this.serverParams.url)
try {
- transport = new StreamableHTTPClientTransport(baseUrl)
+ if (this.serverParams.headers) {
+ transport = new StreamableHTTPClientTransport(baseUrl, {
+ requestInit: {
+ headers: this.serverParams.headers
+ }
+ })
+ } else {
+ transport = new StreamableHTTPClientTransport(baseUrl)
+ }
await client.connect(transport)
} catch (error) {
- transport = new SSEClientTransport(baseUrl)
+ if (this.serverParams.headers) {
+ transport = new SSEClientTransport(baseUrl, {
+ requestInit: {
+ headers: this.serverParams.headers
+ },
+ eventSourceInit: {
+ fetch: (url, init) => fetch(url, { ...init, headers: this.serverParams.headers })
+ }
+ })
+ } else {
+ transport = new SSEClientTransport(baseUrl)
+ }
await client.connect(transport)
}
}
diff --git a/packages/components/nodes/tools/MicrosoftOutlook/MicrosoftOutlook.ts b/packages/components/nodes/tools/MicrosoftOutlook/MicrosoftOutlook.ts
new file mode 100644
index 00000000000..a85af7221ea
--- /dev/null
+++ b/packages/components/nodes/tools/MicrosoftOutlook/MicrosoftOutlook.ts
@@ -0,0 +1,822 @@
+import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils'
+import { createOutlookTools } from './core'
+import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
+
+class MicrosoftOutlook_Tools implements INode {
+ label: string
+ name: string
+ version: number
+ type: string
+ icon: string
+ category: string
+ description: string
+ baseClasses: string[]
+ credential: INodeParams
+ inputs: INodeParams[]
+
+ constructor() {
+ this.label = 'Microsoft Outlook'
+ this.name = 'microsoftOutlook'
+ this.version = 1.0
+ this.type = 'MicrosoftOutlook'
+ this.icon = 'outlook.svg'
+ this.category = 'Tools'
+ this.description = 'Perform Microsoft Outlook operations for calendars, events, and messages'
+ this.baseClasses = [this.type, 'Tool']
+ this.credential = {
+ label: 'Connect Credential',
+ name: 'credential',
+ type: 'credential',
+ credentialNames: ['microsoftOutlookOAuth2']
+ }
+ this.inputs = [
+ {
+ label: 'Type',
+ name: 'outlookType',
+ type: 'options',
+ options: [
+ {
+ label: 'Calendar',
+ name: 'calendar'
+ },
+ {
+ label: 'Message',
+ name: 'message'
+ }
+ ]
+ },
+ // Calendar Actions
+ {
+ label: 'Calendar Actions',
+ name: 'calendarActions',
+ type: 'multiOptions',
+ options: [
+ {
+ label: 'List Calendars',
+ name: 'listCalendars'
+ },
+ {
+ label: 'Get Calendar',
+ name: 'getCalendar'
+ },
+ {
+ label: 'Create Calendar',
+ name: 'createCalendar'
+ },
+ {
+ label: 'Update Calendar',
+ name: 'updateCalendar'
+ },
+ {
+ label: 'Delete Calendar',
+ name: 'deleteCalendar'
+ },
+ {
+ label: 'List Events',
+ name: 'listEvents'
+ },
+ {
+ label: 'Get Event',
+ name: 'getEvent'
+ },
+ {
+ label: 'Create Event',
+ name: 'createEvent'
+ },
+ {
+ label: 'Update Event',
+ name: 'updateEvent'
+ },
+ {
+ label: 'Delete Event',
+ name: 'deleteEvent'
+ }
+ ],
+ show: {
+ outlookType: ['calendar']
+ }
+ },
+ // Message Actions
+ {
+ label: 'Message Actions',
+ name: 'messageActions',
+ type: 'multiOptions',
+ options: [
+ {
+ label: 'List Messages',
+ name: 'listMessages'
+ },
+ {
+ label: 'Get Message',
+ name: 'getMessage'
+ },
+ {
+ label: 'Create Draft Message',
+ name: 'createDraftMessage'
+ },
+ {
+ label: 'Send Message',
+ name: 'sendMessage'
+ },
+ {
+ label: 'Update Message',
+ name: 'updateMessage'
+ },
+ {
+ label: 'Delete Message',
+ name: 'deleteMessage'
+ },
+ {
+ label: 'Copy Message',
+ name: 'copyMessage'
+ },
+ {
+ label: 'Move Message',
+ name: 'moveMessage'
+ },
+ {
+ label: 'Reply to Message',
+ name: 'replyMessage'
+ },
+ {
+ label: 'Forward Message',
+ name: 'forwardMessage'
+ }
+ ],
+ show: {
+ outlookType: ['message']
+ }
+ },
+ // CALENDAR PARAMETERS
+ // List Calendars Parameters
+ {
+ label: 'Max Results [List Calendars]',
+ name: 'maxResultsListCalendars',
+ type: 'number',
+ description: 'Maximum number of calendars to return',
+ default: 50,
+ show: {
+ outlookType: ['calendar'],
+ calendarActions: ['listCalendars']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Get Calendar Parameters
+ {
+ label: 'Calendar ID [Get Calendar]',
+ name: 'calendarIdGetCalendar',
+ type: 'string',
+ description: 'ID of the calendar to retrieve',
+ show: {
+ outlookType: ['calendar'],
+ calendarActions: ['getCalendar']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Create Calendar Parameters
+ {
+ label: 'Calendar Name [Create Calendar]',
+ name: 'calendarNameCreateCalendar',
+ type: 'string',
+ description: 'Name of the calendar',
+ placeholder: 'My New Calendar',
+ show: {
+ outlookType: ['calendar'],
+ calendarActions: ['createCalendar']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Update Calendar Parameters
+ {
+ label: 'Calendar ID [Update Calendar]',
+ name: 'calendarIdUpdateCalendar',
+ type: 'string',
+ description: 'ID of the calendar to update',
+ show: {
+ outlookType: ['calendar'],
+ calendarActions: ['updateCalendar']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Calendar Name [Update Calendar]',
+ name: 'calendarNameUpdateCalendar',
+ type: 'string',
+ description: 'New name of the calendar',
+ show: {
+ outlookType: ['calendar'],
+ calendarActions: ['updateCalendar']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Delete Calendar Parameters
+ {
+ label: 'Calendar ID [Delete Calendar]',
+ name: 'calendarIdDeleteCalendar',
+ type: 'string',
+ description: 'ID of the calendar to delete',
+ show: {
+ outlookType: ['calendar'],
+ calendarActions: ['deleteCalendar']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // List Events Parameters
+ {
+ label: 'Calendar ID [List Events]',
+ name: 'calendarIdListEvents',
+ type: 'string',
+ description: 'ID of the calendar (leave empty for primary calendar)',
+ show: {
+ outlookType: ['calendar'],
+ calendarActions: ['listEvents']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Max Results [List Events]',
+ name: 'maxResultsListEvents',
+ type: 'number',
+ description: 'Maximum number of events to return',
+ default: 50,
+ show: {
+ outlookType: ['calendar'],
+ calendarActions: ['listEvents']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Start Date Time [List Events]',
+ name: 'startDateTimeListEvents',
+ type: 'string',
+ description: 'Start date time filter in ISO format',
+ placeholder: '2024-01-01T00:00:00Z',
+ show: {
+ outlookType: ['calendar'],
+ calendarActions: ['listEvents']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'End Date Time [List Events]',
+ name: 'endDateTimeListEvents',
+ type: 'string',
+ description: 'End date time filter in ISO format',
+ placeholder: '2024-12-31T23:59:59Z',
+ show: {
+ outlookType: ['calendar'],
+ calendarActions: ['listEvents']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Get Event Parameters
+ {
+ label: 'Event ID [Get Event]',
+ name: 'eventIdGetEvent',
+ type: 'string',
+ description: 'ID of the event to retrieve',
+ show: {
+ outlookType: ['calendar'],
+ calendarActions: ['getEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Create Event Parameters
+ {
+ label: 'Subject [Create Event]',
+ name: 'subjectCreateEvent',
+ type: 'string',
+ description: 'Subject/title of the event',
+ placeholder: 'Meeting Title',
+ show: {
+ outlookType: ['calendar'],
+ calendarActions: ['createEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Body [Create Event]',
+ name: 'bodyCreateEvent',
+ type: 'string',
+ description: 'Body/description of the event',
+ placeholder: 'Meeting description',
+ rows: 3,
+ show: {
+ outlookType: ['calendar'],
+ calendarActions: ['createEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Start Date Time [Create Event]',
+ name: 'startDateTimeCreateEvent',
+ type: 'string',
+ description: 'Start date and time in ISO format',
+ placeholder: '2024-01-15T10:00:00',
+ show: {
+ outlookType: ['calendar'],
+ calendarActions: ['createEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'End Date Time [Create Event]',
+ name: 'endDateTimeCreateEvent',
+ type: 'string',
+ description: 'End date and time in ISO format',
+ placeholder: '2024-01-15T11:00:00',
+ show: {
+ outlookType: ['calendar'],
+ calendarActions: ['createEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Time Zone [Create Event]',
+ name: 'timeZoneCreateEvent',
+ type: 'string',
+ description: 'Time zone for the event',
+ placeholder: 'UTC',
+ default: 'UTC',
+ show: {
+ outlookType: ['calendar'],
+ calendarActions: ['createEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Location [Create Event]',
+ name: 'locationCreateEvent',
+ type: 'string',
+ description: 'Location of the event',
+ placeholder: 'Conference Room A',
+ show: {
+ outlookType: ['calendar'],
+ calendarActions: ['createEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Attendees [Create Event]',
+ name: 'attendeesCreateEvent',
+ type: 'string',
+ description: 'Comma-separated list of attendee email addresses',
+ placeholder: 'user1@example.com,user2@example.com',
+ show: {
+ outlookType: ['calendar'],
+ calendarActions: ['createEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Update Event Parameters
+ {
+ label: 'Event ID [Update Event]',
+ name: 'eventIdUpdateEvent',
+ type: 'string',
+ description: 'ID of the event to update',
+ show: {
+ outlookType: ['calendar'],
+ calendarActions: ['updateEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Subject [Update Event]',
+ name: 'subjectUpdateEvent',
+ type: 'string',
+ description: 'New subject/title of the event',
+ show: {
+ outlookType: ['calendar'],
+ calendarActions: ['updateEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Delete Event Parameters
+ {
+ label: 'Event ID [Delete Event]',
+ name: 'eventIdDeleteEvent',
+ type: 'string',
+ description: 'ID of the event to delete',
+ show: {
+ outlookType: ['calendar'],
+ calendarActions: ['deleteEvent']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // MESSAGE PARAMETERS
+ // List Messages Parameters
+ {
+ label: 'Max Results [List Messages]',
+ name: 'maxResultsListMessages',
+ type: 'number',
+ description: 'Maximum number of messages to return',
+ default: 50,
+ show: {
+ outlookType: ['message'],
+ messageActions: ['listMessages']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Filter [List Messages]',
+ name: 'filterListMessages',
+ type: 'string',
+ description: 'Filter query (e.g., "isRead eq false")',
+ show: {
+ outlookType: ['message'],
+ messageActions: ['listMessages']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Get Message Parameters
+ {
+ label: 'Message ID [Get Message]',
+ name: 'messageIdGetMessage',
+ type: 'string',
+ description: 'ID of the message to retrieve',
+ show: {
+ outlookType: ['message'],
+ messageActions: ['getMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Create Draft Message Parameters
+ {
+ label: 'To [Create Draft Message]',
+ name: 'toCreateDraftMessage',
+ type: 'string',
+ description: 'Recipient email address(es), comma-separated',
+ placeholder: 'user@example.com',
+ show: {
+ outlookType: ['message'],
+ messageActions: ['createDraftMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Subject [Create Draft Message]',
+ name: 'subjectCreateDraftMessage',
+ type: 'string',
+ description: 'Subject of the message',
+ placeholder: 'Email Subject',
+ show: {
+ outlookType: ['message'],
+ messageActions: ['createDraftMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Body [Create Draft Message]',
+ name: 'bodyCreateDraftMessage',
+ type: 'string',
+ description: 'Body content of the message',
+ placeholder: 'Email body content',
+ rows: 4,
+ show: {
+ outlookType: ['message'],
+ messageActions: ['createDraftMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'CC [Create Draft Message]',
+ name: 'ccCreateDraftMessage',
+ type: 'string',
+ description: 'CC email address(es), comma-separated',
+ placeholder: 'cc@example.com',
+ show: {
+ outlookType: ['message'],
+ messageActions: ['createDraftMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'BCC [Create Draft Message]',
+ name: 'bccCreateDraftMessage',
+ type: 'string',
+ description: 'BCC email address(es), comma-separated',
+ placeholder: 'bcc@example.com',
+ show: {
+ outlookType: ['message'],
+ messageActions: ['createDraftMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Send Message Parameters
+ {
+ label: 'To [Send Message]',
+ name: 'toSendMessage',
+ type: 'string',
+ description: 'Recipient email address(es), comma-separated',
+ placeholder: 'user@example.com',
+ show: {
+ outlookType: ['message'],
+ messageActions: ['sendMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Subject [Send Message]',
+ name: 'subjectSendMessage',
+ type: 'string',
+ description: 'Subject of the message',
+ placeholder: 'Email Subject',
+ show: {
+ outlookType: ['message'],
+ messageActions: ['sendMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Body [Send Message]',
+ name: 'bodySendMessage',
+ type: 'string',
+ description: 'Body content of the message',
+ placeholder: 'Email body content',
+ rows: 4,
+ show: {
+ outlookType: ['message'],
+ messageActions: ['sendMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Update Message Parameters
+ {
+ label: 'Message ID [Update Message]',
+ name: 'messageIdUpdateMessage',
+ type: 'string',
+ description: 'ID of the message to update',
+ show: {
+ outlookType: ['message'],
+ messageActions: ['updateMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Is Read [Update Message]',
+ name: 'isReadUpdateMessage',
+ type: 'boolean',
+ description: 'Mark message as read/unread',
+ show: {
+ outlookType: ['message'],
+ messageActions: ['updateMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Delete Message Parameters
+ {
+ label: 'Message ID [Delete Message]',
+ name: 'messageIdDeleteMessage',
+ type: 'string',
+ description: 'ID of the message to delete',
+ show: {
+ outlookType: ['message'],
+ messageActions: ['deleteMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Copy Message Parameters
+ {
+ label: 'Message ID [Copy Message]',
+ name: 'messageIdCopyMessage',
+ type: 'string',
+ description: 'ID of the message to copy',
+ show: {
+ outlookType: ['message'],
+ messageActions: ['copyMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Destination Folder ID [Copy Message]',
+ name: 'destinationFolderIdCopyMessage',
+ type: 'string',
+ description: 'ID of the destination folder',
+ show: {
+ outlookType: ['message'],
+ messageActions: ['copyMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Move Message Parameters
+ {
+ label: 'Message ID [Move Message]',
+ name: 'messageIdMoveMessage',
+ type: 'string',
+ description: 'ID of the message to move',
+ show: {
+ outlookType: ['message'],
+ messageActions: ['moveMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Destination Folder ID [Move Message]',
+ name: 'destinationFolderIdMoveMessage',
+ type: 'string',
+ description: 'ID of the destination folder',
+ show: {
+ outlookType: ['message'],
+ messageActions: ['moveMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Reply Message Parameters
+ {
+ label: 'Message ID [Reply Message]',
+ name: 'messageIdReplyMessage',
+ type: 'string',
+ description: 'ID of the message to reply to',
+ show: {
+ outlookType: ['message'],
+ messageActions: ['replyMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Reply Body [Reply Message]',
+ name: 'replyBodyReplyMessage',
+ type: 'string',
+ description: 'Reply message body',
+ rows: 4,
+ show: {
+ outlookType: ['message'],
+ messageActions: ['replyMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ // Forward Message Parameters
+ {
+ label: 'Message ID [Forward Message]',
+ name: 'messageIdForwardMessage',
+ type: 'string',
+ description: 'ID of the message to forward',
+ show: {
+ outlookType: ['message'],
+ messageActions: ['forwardMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Forward To [Forward Message]',
+ name: 'forwardToForwardMessage',
+ type: 'string',
+ description: 'Email address(es) to forward to, comma-separated',
+ show: {
+ outlookType: ['message'],
+ messageActions: ['forwardMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Forward Comment [Forward Message]',
+ name: 'forwardCommentForwardMessage',
+ type: 'string',
+ description: 'Additional comment to include with forward',
+ rows: 2,
+ show: {
+ outlookType: ['message'],
+ messageActions: ['forwardMessage']
+ },
+ additionalParams: true,
+ optional: true
+ }
+ ]
+ }
+
+ async init(nodeData: INodeData, _: string, options: ICommonObject): Promise {
+ const outlookType = nodeData.inputs?.outlookType as string
+ const calendarActions = nodeData.inputs?.calendarActions as string
+ const messageActions = nodeData.inputs?.messageActions as string
+
+ let credentialData = await getCredentialData(nodeData.credential ?? '', options)
+ credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options)
+ const accessToken = getCredentialParam('access_token', credentialData, nodeData)
+
+ if (!accessToken) {
+ throw new Error('No access token found in credential')
+ }
+
+ let actions: string[] = []
+ if (outlookType === 'calendar') {
+ actions = convertMultiOptionsToStringArray(calendarActions)
+ } else if (outlookType === 'message') {
+ actions = convertMultiOptionsToStringArray(messageActions)
+ }
+
+ const defaultParams = this.transformNodeInputsToToolArgs(nodeData)
+
+ const outlookTools = createOutlookTools({
+ accessToken,
+ actions,
+ defaultParams
+ })
+
+ return outlookTools
+ }
+
+ transformNodeInputsToToolArgs(nodeData: INodeData): Record {
+ // Collect default parameters from inputs
+ const defaultParams: Record = {}
+
+ // Calendar parameters
+ if (nodeData.inputs?.maxResultsListCalendars) defaultParams.maxResultsListCalendars = nodeData.inputs.maxResultsListCalendars
+ if (nodeData.inputs?.calendarIdGetCalendar) defaultParams.calendarIdGetCalendar = nodeData.inputs.calendarIdGetCalendar
+ if (nodeData.inputs?.calendarNameCreateCalendar)
+ defaultParams.calendarNameCreateCalendar = nodeData.inputs.calendarNameCreateCalendar
+ if (nodeData.inputs?.calendarIdUpdateCalendar) defaultParams.calendarIdUpdateCalendar = nodeData.inputs.calendarIdUpdateCalendar
+ if (nodeData.inputs?.calendarNameUpdateCalendar)
+ defaultParams.calendarNameUpdateCalendar = nodeData.inputs.calendarNameUpdateCalendar
+ if (nodeData.inputs?.calendarIdDeleteCalendar) defaultParams.calendarIdDeleteCalendar = nodeData.inputs.calendarIdDeleteCalendar
+ if (nodeData.inputs?.calendarIdListEvents) defaultParams.calendarIdListEvents = nodeData.inputs.calendarIdListEvents
+ if (nodeData.inputs?.maxResultsListEvents) defaultParams.maxResultsListEvents = nodeData.inputs.maxResultsListEvents
+ if (nodeData.inputs?.startDateTimeListEvents) defaultParams.startDateTimeListEvents = nodeData.inputs.startDateTimeListEvents
+ if (nodeData.inputs?.endDateTimeListEvents) defaultParams.endDateTimeListEvents = nodeData.inputs.endDateTimeListEvents
+ if (nodeData.inputs?.eventIdGetEvent) defaultParams.eventIdGetEvent = nodeData.inputs.eventIdGetEvent
+ if (nodeData.inputs?.subjectCreateEvent) defaultParams.subjectCreateEvent = nodeData.inputs.subjectCreateEvent
+ if (nodeData.inputs?.bodyCreateEvent) defaultParams.bodyCreateEvent = nodeData.inputs.bodyCreateEvent
+ if (nodeData.inputs?.startDateTimeCreateEvent) defaultParams.startDateTimeCreateEvent = nodeData.inputs.startDateTimeCreateEvent
+ if (nodeData.inputs?.endDateTimeCreateEvent) defaultParams.endDateTimeCreateEvent = nodeData.inputs.endDateTimeCreateEvent
+ if (nodeData.inputs?.timeZoneCreateEvent) defaultParams.timeZoneCreateEvent = nodeData.inputs.timeZoneCreateEvent
+ if (nodeData.inputs?.locationCreateEvent) defaultParams.locationCreateEvent = nodeData.inputs.locationCreateEvent
+ if (nodeData.inputs?.attendeesCreateEvent) defaultParams.attendeesCreateEvent = nodeData.inputs.attendeesCreateEvent
+ if (nodeData.inputs?.eventIdUpdateEvent) defaultParams.eventIdUpdateEvent = nodeData.inputs.eventIdUpdateEvent
+ if (nodeData.inputs?.subjectUpdateEvent) defaultParams.subjectUpdateEvent = nodeData.inputs.subjectUpdateEvent
+ if (nodeData.inputs?.eventIdDeleteEvent) defaultParams.eventIdDeleteEvent = nodeData.inputs.eventIdDeleteEvent
+
+ // Message parameters
+ if (nodeData.inputs?.maxResultsListMessages) defaultParams.maxResultsListMessages = nodeData.inputs.maxResultsListMessages
+ if (nodeData.inputs?.filterListMessages) defaultParams.filterListMessages = nodeData.inputs.filterListMessages
+ if (nodeData.inputs?.messageIdGetMessage) defaultParams.messageIdGetMessage = nodeData.inputs.messageIdGetMessage
+ if (nodeData.inputs?.toCreateDraftMessage) defaultParams.toCreateDraftMessage = nodeData.inputs.toCreateDraftMessage
+ if (nodeData.inputs?.subjectCreateDraftMessage) defaultParams.subjectCreateDraftMessage = nodeData.inputs.subjectCreateDraftMessage
+ if (nodeData.inputs?.bodyCreateDraftMessage) defaultParams.bodyCreateDraftMessage = nodeData.inputs.bodyCreateDraftMessage
+ if (nodeData.inputs?.ccCreateDraftMessage) defaultParams.ccCreateDraftMessage = nodeData.inputs.ccCreateDraftMessage
+ if (nodeData.inputs?.bccCreateDraftMessage) defaultParams.bccCreateDraftMessage = nodeData.inputs.bccCreateDraftMessage
+ if (nodeData.inputs?.toSendMessage) defaultParams.toSendMessage = nodeData.inputs.toSendMessage
+ if (nodeData.inputs?.subjectSendMessage) defaultParams.subjectSendMessage = nodeData.inputs.subjectSendMessage
+ if (nodeData.inputs?.bodySendMessage) defaultParams.bodySendMessage = nodeData.inputs.bodySendMessage
+ if (nodeData.inputs?.messageIdUpdateMessage) defaultParams.messageIdUpdateMessage = nodeData.inputs.messageIdUpdateMessage
+ if (nodeData.inputs?.isReadUpdateMessage !== undefined) defaultParams.isReadUpdateMessage = nodeData.inputs.isReadUpdateMessage
+ if (nodeData.inputs?.messageIdDeleteMessage) defaultParams.messageIdDeleteMessage = nodeData.inputs.messageIdDeleteMessage
+ if (nodeData.inputs?.messageIdCopyMessage) defaultParams.messageIdCopyMessage = nodeData.inputs.messageIdCopyMessage
+ if (nodeData.inputs?.destinationFolderIdCopyMessage)
+ defaultParams.destinationFolderIdCopyMessage = nodeData.inputs.destinationFolderIdCopyMessage
+ if (nodeData.inputs?.messageIdMoveMessage) defaultParams.messageIdMoveMessage = nodeData.inputs.messageIdMoveMessage
+ if (nodeData.inputs?.destinationFolderIdMoveMessage)
+ defaultParams.destinationFolderIdMoveMessage = nodeData.inputs.destinationFolderIdMoveMessage
+ if (nodeData.inputs?.messageIdReplyMessage) defaultParams.messageIdReplyMessage = nodeData.inputs.messageIdReplyMessage
+ if (nodeData.inputs?.replyBodyReplyMessage) defaultParams.replyBodyReplyMessage = nodeData.inputs.replyBodyReplyMessage
+ if (nodeData.inputs?.messageIdForwardMessage) defaultParams.messageIdForwardMessage = nodeData.inputs.messageIdForwardMessage
+ if (nodeData.inputs?.forwardToForwardMessage) defaultParams.forwardToForwardMessage = nodeData.inputs.forwardToForwardMessage
+ if (nodeData.inputs?.forwardCommentForwardMessage)
+ defaultParams.forwardCommentForwardMessage = nodeData.inputs.forwardCommentForwardMessage
+
+ return defaultParams
+ }
+}
+
+module.exports = { nodeClass: MicrosoftOutlook_Tools }
diff --git a/packages/components/nodes/tools/MicrosoftOutlook/core.ts b/packages/components/nodes/tools/MicrosoftOutlook/core.ts
new file mode 100644
index 00000000000..ce6fe8ba8fc
--- /dev/null
+++ b/packages/components/nodes/tools/MicrosoftOutlook/core.ts
@@ -0,0 +1,1029 @@
+import { z } from 'zod'
+import fetch from 'node-fetch'
+import { DynamicStructuredTool } from '../OpenAPIToolkit/core'
+import { TOOL_ARGS_PREFIX } from '../../../src/agents'
+
+export const desc = `Use this when you want to access Microsoft Outlook API for managing calendars, events, and messages`
+
+export interface Headers {
+ [key: string]: string
+}
+
+export interface Body {
+ [key: string]: any
+}
+
+export interface RequestParameters {
+ headers?: Headers
+ body?: Body
+ url?: string
+ description?: string
+ name?: string
+ actions?: string[]
+ accessToken?: string
+ defaultParams?: any
+}
+
+// Define schemas for different Outlook operations
+
+// Calendar Schemas
+const ListCalendarsSchema = z.object({
+ maxResults: z.number().optional().default(50).describe('Maximum number of calendars to return')
+})
+
+const GetCalendarSchema = z.object({
+ calendarId: z.string().describe('ID of the calendar to retrieve')
+})
+
+const CreateCalendarSchema = z.object({
+ calendarName: z.string().describe('Name of the calendar')
+})
+
+const UpdateCalendarSchema = z.object({
+ calendarId: z.string().describe('ID of the calendar to update'),
+ calendarName: z.string().describe('New name of the calendar')
+})
+
+const DeleteCalendarSchema = z.object({
+ calendarId: z.string().describe('ID of the calendar to delete')
+})
+
+const ListEventsSchema = z.object({
+ calendarId: z.string().optional().describe('ID of the calendar (empty for primary calendar)'),
+ maxResults: z.number().optional().default(50).describe('Maximum number of events to return'),
+ startDateTime: z.string().optional().describe('Start date time filter in ISO format'),
+ endDateTime: z.string().optional().describe('End date time filter in ISO format')
+})
+
+const GetEventSchema = z.object({
+ eventId: z.string().describe('ID of the event to retrieve')
+})
+
+const CreateEventSchema = z.object({
+ subject: z.string().describe('Subject/title of the event'),
+ body: z.string().optional().describe('Body/description of the event'),
+ startDateTime: z.string().describe('Start date and time in ISO format'),
+ endDateTime: z.string().describe('End date and time in ISO format'),
+ timeZone: z.string().optional().default('UTC').describe('Time zone for the event'),
+ location: z.string().optional().describe('Location of the event'),
+ attendees: z.string().optional().describe('Comma-separated list of attendee email addresses')
+})
+
+const UpdateEventSchema = z.object({
+ eventId: z.string().describe('ID of the event to update'),
+ subject: z.string().optional().describe('New subject/title of the event')
+})
+
+const DeleteEventSchema = z.object({
+ eventId: z.string().describe('ID of the event to delete')
+})
+
+// Message Schemas
+const ListMessagesSchema = z.object({
+ maxResults: z.number().optional().default(50).describe('Maximum number of messages to return'),
+ filter: z.string().optional().describe('Filter query (e.g., "isRead eq false")')
+})
+
+const GetMessageSchema = z.object({
+ messageId: z.string().describe('ID of the message to retrieve')
+})
+
+const CreateDraftMessageSchema = z.object({
+ to: z.string().describe('Recipient email address(es), comma-separated'),
+ subject: z.string().optional().describe('Subject of the message'),
+ body: z.string().optional().describe('Body content of the message'),
+ cc: z.string().optional().describe('CC email address(es), comma-separated'),
+ bcc: z.string().optional().describe('BCC email address(es), comma-separated')
+})
+
+const SendMessageSchema = z.object({
+ to: z.string().describe('Recipient email address(es), comma-separated'),
+ subject: z.string().optional().describe('Subject of the message'),
+ body: z.string().optional().describe('Body content of the message')
+})
+
+const UpdateMessageSchema = z.object({
+ messageId: z.string().describe('ID of the message to update'),
+ isRead: z.boolean().optional().describe('Mark message as read/unread')
+})
+
+const DeleteMessageSchema = z.object({
+ messageId: z.string().describe('ID of the message to delete')
+})
+
+const CopyMessageSchema = z.object({
+ messageId: z.string().describe('ID of the message to copy'),
+ destinationFolderId: z.string().describe('ID of the destination folder')
+})
+
+const MoveMessageSchema = z.object({
+ messageId: z.string().describe('ID of the message to move'),
+ destinationFolderId: z.string().describe('ID of the destination folder')
+})
+
+const ReplyMessageSchema = z.object({
+ messageId: z.string().describe('ID of the message to reply to'),
+ replyBody: z.string().describe('Reply message body')
+})
+
+const ForwardMessageSchema = z.object({
+ messageId: z.string().describe('ID of the message to forward'),
+ forwardTo: z.string().describe('Email address(es) to forward to, comma-separated'),
+ forwardComment: z.string().optional().describe('Additional comment to include with forward')
+})
+
+class BaseOutlookTool extends DynamicStructuredTool {
+ protected accessToken: string = ''
+
+ constructor(args: any) {
+ super(args)
+ this.accessToken = args.accessToken ?? ''
+ }
+
+ async makeGraphRequest(url: string, method: string = 'GET', body?: any, params?: any): Promise {
+ const headers = {
+ Authorization: `Bearer ${this.accessToken}`,
+ 'Content-Type': 'application/json',
+ ...this.headers
+ }
+
+ const response = await fetch(url, {
+ method,
+ headers,
+ body: body ? JSON.stringify(body) : undefined
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ throw new Error(`Graph API Error ${response.status}: ${response.statusText} - ${errorText}`)
+ }
+
+ const data = await response.text()
+ return data + TOOL_ARGS_PREFIX + JSON.stringify(params)
+ }
+
+ parseEmailAddresses(emailString: string) {
+ return emailString.split(',').map((email) => ({
+ emailAddress: {
+ address: email.trim(),
+ name: email.trim()
+ }
+ }))
+ }
+}
+
+// Calendar Tools
+class ListCalendarsTool extends BaseOutlookTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'list_calendars',
+ description: 'List calendars in Microsoft Outlook',
+ schema: ListCalendarsSchema,
+ baseUrl: 'https://graph.microsoft.com/v1.0/me/calendars',
+ method: 'GET',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const queryParams = new URLSearchParams()
+
+ if (params.maxResults) queryParams.append('$top', params.maxResults.toString())
+
+ const url = `https://graph.microsoft.com/v1.0/me/calendars?${queryParams.toString()}`
+
+ try {
+ const response = await this.makeGraphRequest(url, 'GET', undefined, params)
+ return response
+ } catch (error) {
+ return `Error listing calendars: ${error}`
+ }
+ }
+}
+
+class GetCalendarTool extends BaseOutlookTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'get_calendar',
+ description: 'Get a specific calendar by ID from Microsoft Outlook',
+ schema: GetCalendarSchema,
+ baseUrl: 'https://graph.microsoft.com/v1.0/me/calendars',
+ method: 'GET',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const url = `https://graph.microsoft.com/v1.0/me/calendars/${params.calendarId}`
+
+ try {
+ const response = await this.makeGraphRequest(url, 'GET', undefined, params)
+ return response
+ } catch (error) {
+ return `Error getting calendar: ${error}`
+ }
+ }
+}
+
+class CreateCalendarTool extends BaseOutlookTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'create_calendar',
+ description: 'Create a new calendar in Microsoft Outlook',
+ schema: CreateCalendarSchema,
+ baseUrl: 'https://graph.microsoft.com/v1.0/me/calendars',
+ method: 'POST',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const calendarData = {
+ name: params.calendarName
+ }
+
+ const url = 'https://graph.microsoft.com/v1.0/me/calendars'
+ const response = await this.makeGraphRequest(url, 'POST', calendarData, params)
+ return response
+ } catch (error) {
+ return `Error creating calendar: ${error}`
+ }
+ }
+}
+
+class UpdateCalendarTool extends BaseOutlookTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'update_calendar',
+ description: 'Update a calendar in Microsoft Outlook',
+ schema: UpdateCalendarSchema,
+ baseUrl: 'https://graph.microsoft.com/v1.0/me/calendars',
+ method: 'PATCH',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const calendarData = {
+ name: params.calendarName
+ }
+
+ const url = `https://graph.microsoft.com/v1.0/me/calendars/${params.calendarId}`
+ const response = await this.makeGraphRequest(url, 'PATCH', calendarData, params)
+ return response
+ } catch (error) {
+ return `Error updating calendar: ${error}`
+ }
+ }
+}
+
+class DeleteCalendarTool extends BaseOutlookTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'delete_calendar',
+ description: 'Delete a calendar from Microsoft Outlook',
+ schema: DeleteCalendarSchema,
+ baseUrl: 'https://graph.microsoft.com/v1.0/me/calendars',
+ method: 'DELETE',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const url = `https://graph.microsoft.com/v1.0/me/calendars/${params.calendarId}`
+
+ try {
+ await this.makeGraphRequest(url, 'DELETE', undefined, params)
+ return `Calendar ${params.calendarId} deleted successfully`
+ } catch (error) {
+ return `Error deleting calendar: ${error}`
+ }
+ }
+}
+
+class ListEventsTool extends BaseOutlookTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'list_events',
+ description: 'List events from Microsoft Outlook calendar',
+ schema: ListEventsSchema,
+ baseUrl: 'https://graph.microsoft.com/v1.0/me/events',
+ method: 'GET',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const queryParams = new URLSearchParams()
+
+ if (params.maxResults) queryParams.append('$top', params.maxResults.toString())
+ if (params.startDateTime) queryParams.append('$filter', `start/dateTime ge '${params.startDateTime}'`)
+ if (params.endDateTime) {
+ const existingFilter = queryParams.get('$filter')
+ const endFilter = `end/dateTime le '${params.endDateTime}'`
+ if (existingFilter) {
+ queryParams.set('$filter', `${existingFilter} and ${endFilter}`)
+ } else {
+ queryParams.append('$filter', endFilter)
+ }
+ }
+
+ const baseUrl = params.calendarId
+ ? `https://graph.microsoft.com/v1.0/me/calendars/${params.calendarId}/events`
+ : 'https://graph.microsoft.com/v1.0/me/events'
+
+ const url = `${baseUrl}?${queryParams.toString()}`
+
+ try {
+ const response = await this.makeGraphRequest(url, 'GET', undefined, params)
+ return response
+ } catch (error) {
+ return `Error listing events: ${error}`
+ }
+ }
+}
+
+class GetEventTool extends BaseOutlookTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'get_event',
+ description: 'Get a specific event by ID from Microsoft Outlook',
+ schema: GetEventSchema,
+ baseUrl: 'https://graph.microsoft.com/v1.0/me/events',
+ method: 'GET',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const url = `https://graph.microsoft.com/v1.0/me/events/${params.eventId}`
+
+ try {
+ const response = await this.makeGraphRequest(url, 'GET', undefined, params)
+ return response
+ } catch (error) {
+ return `Error getting event: ${error}`
+ }
+ }
+}
+
+class CreateEventTool extends BaseOutlookTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'create_event',
+ description: 'Create a new event in Microsoft Outlook calendar',
+ schema: CreateEventSchema,
+ baseUrl: 'https://graph.microsoft.com/v1.0/me/events',
+ method: 'POST',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const eventData = {
+ subject: params.subject,
+ body: {
+ contentType: 'HTML',
+ content: params.body || ''
+ },
+ start: {
+ dateTime: params.startDateTime,
+ timeZone: params.timeZone || 'UTC'
+ },
+ end: {
+ dateTime: params.endDateTime,
+ timeZone: params.timeZone || 'UTC'
+ },
+ location: params.location
+ ? {
+ displayName: params.location
+ }
+ : undefined,
+ attendees: params.attendees ? this.parseEmailAddresses(params.attendees) : []
+ }
+
+ const url = 'https://graph.microsoft.com/v1.0/me/events'
+ const response = await this.makeGraphRequest(url, 'POST', eventData, params)
+ return response
+ } catch (error) {
+ return `Error creating event: ${error}`
+ }
+ }
+}
+
+class UpdateEventTool extends BaseOutlookTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'update_event',
+ description: 'Update an event in Microsoft Outlook calendar',
+ schema: UpdateEventSchema,
+ baseUrl: 'https://graph.microsoft.com/v1.0/me/events',
+ method: 'PATCH',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const eventData: any = {}
+ if (params.subject) eventData.subject = params.subject
+
+ const url = `https://graph.microsoft.com/v1.0/me/events/${params.eventId}`
+ const response = await this.makeGraphRequest(url, 'PATCH', eventData, params)
+ return response
+ } catch (error) {
+ return `Error updating event: ${error}`
+ }
+ }
+}
+
+class DeleteEventTool extends BaseOutlookTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'delete_event',
+ description: 'Delete an event from Microsoft Outlook calendar',
+ schema: DeleteEventSchema,
+ baseUrl: 'https://graph.microsoft.com/v1.0/me/events',
+ method: 'DELETE',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const url = `https://graph.microsoft.com/v1.0/me/events/${params.eventId}`
+
+ try {
+ await this.makeGraphRequest(url, 'DELETE', undefined, params)
+ return `Event ${params.eventId} deleted successfully`
+ } catch (error) {
+ return `Error deleting event: ${error}`
+ }
+ }
+}
+
+// Message Tools
+class ListMessagesTool extends BaseOutlookTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'list_messages',
+ description: 'List messages from Microsoft Outlook mailbox',
+ schema: ListMessagesSchema,
+ baseUrl: 'https://graph.microsoft.com/v1.0/me/messages',
+ method: 'GET',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const queryParams = new URLSearchParams()
+
+ if (params.maxResults) queryParams.append('$top', params.maxResults.toString())
+ if (params.filter) queryParams.append('$filter', params.filter)
+
+ const url = `https://graph.microsoft.com/v1.0/me/messages?${queryParams.toString()}`
+
+ try {
+ const response = await this.makeGraphRequest(url, 'GET', undefined, params)
+ return response
+ } catch (error) {
+ return `Error listing messages: ${error}`
+ }
+ }
+}
+
+class GetMessageTool extends BaseOutlookTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'get_message',
+ description: 'Get a specific message by ID from Microsoft Outlook',
+ schema: GetMessageSchema,
+ baseUrl: 'https://graph.microsoft.com/v1.0/me/messages',
+ method: 'GET',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}`
+
+ try {
+ const response = await this.makeGraphRequest(url, 'GET', undefined, params)
+ return response
+ } catch (error) {
+ return `Error getting message: ${error}`
+ }
+ }
+}
+
+class CreateDraftMessageTool extends BaseOutlookTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'create_draft_message',
+ description: 'Create a draft message in Microsoft Outlook',
+ schema: CreateDraftMessageSchema,
+ baseUrl: 'https://graph.microsoft.com/v1.0/me/messages',
+ method: 'POST',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const messageData = {
+ subject: params.subject || '',
+ body: {
+ contentType: 'HTML',
+ content: params.body || ''
+ },
+ toRecipients: this.parseEmailAddresses(params.to),
+ ccRecipients: params.cc ? this.parseEmailAddresses(params.cc) : [],
+ bccRecipients: params.bcc ? this.parseEmailAddresses(params.bcc) : []
+ }
+
+ const url = 'https://graph.microsoft.com/v1.0/me/messages'
+ const response = await this.makeGraphRequest(url, 'POST', messageData, params)
+ return response
+ } catch (error) {
+ return `Error creating draft message: ${error}`
+ }
+ }
+}
+
+class SendMessageTool extends BaseOutlookTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'send_message',
+ description: 'Send a message via Microsoft Outlook',
+ schema: SendMessageSchema,
+ baseUrl: 'https://graph.microsoft.com/v1.0/me/sendMail',
+ method: 'POST',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const messageData = {
+ message: {
+ subject: params.subject || '',
+ body: {
+ contentType: 'HTML',
+ content: params.body || ''
+ },
+ toRecipients: this.parseEmailAddresses(params.to)
+ },
+ saveToSentItems: true
+ }
+
+ const url = 'https://graph.microsoft.com/v1.0/me/sendMail'
+ await this.makeGraphRequest(url, 'POST', messageData, params)
+ return 'Message sent successfully'
+ } catch (error) {
+ return `Error sending message: ${error}`
+ }
+ }
+}
+
+class UpdateMessageTool extends BaseOutlookTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'update_message',
+ description: 'Update a message in Microsoft Outlook',
+ schema: UpdateMessageSchema,
+ baseUrl: 'https://graph.microsoft.com/v1.0/me/messages',
+ method: 'PATCH',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const messageData: any = {}
+ if (params.isRead !== undefined) messageData.isRead = params.isRead
+
+ const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}`
+ const response = await this.makeGraphRequest(url, 'PATCH', messageData, params)
+ return response
+ } catch (error) {
+ return `Error updating message: ${error}`
+ }
+ }
+}
+
+class DeleteMessageTool extends BaseOutlookTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'delete_message',
+ description: 'Delete a message from Microsoft Outlook',
+ schema: DeleteMessageSchema,
+ baseUrl: 'https://graph.microsoft.com/v1.0/me/messages',
+ method: 'DELETE',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}`
+
+ try {
+ await this.makeGraphRequest(url, 'DELETE', undefined, params)
+ return `Message ${params.messageId} deleted successfully`
+ } catch (error) {
+ return `Error deleting message: ${error}`
+ }
+ }
+}
+
+class CopyMessageTool extends BaseOutlookTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'copy_message',
+ description: 'Copy a message to another folder in Microsoft Outlook',
+ schema: CopyMessageSchema,
+ baseUrl: 'https://graph.microsoft.com/v1.0/me/messages',
+ method: 'POST',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const copyData = {
+ destinationId: params.destinationFolderId
+ }
+
+ const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}/copy`
+ const response = await this.makeGraphRequest(url, 'POST', copyData, params)
+ return response
+ } catch (error) {
+ return `Error copying message: ${error}`
+ }
+ }
+}
+
+class MoveMessageTool extends BaseOutlookTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'move_message',
+ description: 'Move a message to another folder in Microsoft Outlook',
+ schema: MoveMessageSchema,
+ baseUrl: 'https://graph.microsoft.com/v1.0/me/messages',
+ method: 'POST',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const moveData = {
+ destinationId: params.destinationFolderId
+ }
+
+ const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}/move`
+ const response = await this.makeGraphRequest(url, 'POST', moveData, params)
+ return response
+ } catch (error) {
+ return `Error moving message: ${error}`
+ }
+ }
+}
+
+class ReplyMessageTool extends BaseOutlookTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'reply_message',
+ description: 'Reply to a message in Microsoft Outlook',
+ schema: ReplyMessageSchema,
+ baseUrl: 'https://graph.microsoft.com/v1.0/me/messages',
+ method: 'POST',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const replyData = {
+ comment: params.replyBody
+ }
+
+ const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}/reply`
+ await this.makeGraphRequest(url, 'POST', replyData, params)
+ return 'Reply sent successfully'
+ } catch (error) {
+ return `Error replying to message: ${error}`
+ }
+ }
+}
+
+class ForwardMessageTool extends BaseOutlookTool {
+ defaultParams: any
+
+ constructor(args: any) {
+ const toolInput = {
+ name: 'forward_message',
+ description: 'Forward a message in Microsoft Outlook',
+ schema: ForwardMessageSchema,
+ baseUrl: 'https://graph.microsoft.com/v1.0/me/messages',
+ method: 'POST',
+ headers: {}
+ }
+ super({ ...toolInput, accessToken: args.accessToken })
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+
+ try {
+ const forwardData = {
+ toRecipients: this.parseEmailAddresses(params.forwardTo),
+ comment: params.forwardComment || ''
+ }
+
+ const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}/forward`
+ await this.makeGraphRequest(url, 'POST', forwardData, params)
+ return 'Message forwarded successfully'
+ } catch (error) {
+ return `Error forwarding message: ${error}`
+ }
+ }
+}
+
+export const createOutlookTools = (args?: RequestParameters): DynamicStructuredTool[] => {
+ const tools: DynamicStructuredTool[] = []
+ const actions = args?.actions || []
+ const accessToken = args?.accessToken || ''
+ const defaultParams = args?.defaultParams || {}
+
+ // Calendar tools
+ if (actions.includes('listCalendars')) {
+ const listTool = new ListCalendarsTool({
+ accessToken,
+ defaultParams: defaultParams.listCalendars
+ })
+ tools.push(listTool)
+ }
+
+ if (actions.includes('getCalendar')) {
+ const getTool = new GetCalendarTool({
+ accessToken,
+ defaultParams: defaultParams.getCalendar
+ })
+ tools.push(getTool)
+ }
+
+ if (actions.includes('createCalendar')) {
+ const createTool = new CreateCalendarTool({
+ accessToken,
+ defaultParams: defaultParams.createCalendar
+ })
+ tools.push(createTool)
+ }
+
+ if (actions.includes('updateCalendar')) {
+ const updateTool = new UpdateCalendarTool({
+ accessToken,
+ defaultParams: defaultParams.updateCalendar
+ })
+ tools.push(updateTool)
+ }
+
+ if (actions.includes('deleteCalendar')) {
+ const deleteTool = new DeleteCalendarTool({
+ accessToken,
+ defaultParams: defaultParams.deleteCalendar
+ })
+ tools.push(deleteTool)
+ }
+
+ if (actions.includes('listEvents')) {
+ const listTool = new ListEventsTool({
+ accessToken,
+ defaultParams: defaultParams.listEvents
+ })
+ tools.push(listTool)
+ }
+
+ if (actions.includes('getEvent')) {
+ const getTool = new GetEventTool({
+ accessToken,
+ defaultParams: defaultParams.getEvent
+ })
+ tools.push(getTool)
+ }
+
+ if (actions.includes('createEvent')) {
+ const createTool = new CreateEventTool({
+ accessToken,
+ defaultParams: defaultParams.createEvent
+ })
+ tools.push(createTool)
+ }
+
+ if (actions.includes('updateEvent')) {
+ const updateTool = new UpdateEventTool({
+ accessToken,
+ defaultParams: defaultParams.updateEvent
+ })
+ tools.push(updateTool)
+ }
+
+ if (actions.includes('deleteEvent')) {
+ const deleteTool = new DeleteEventTool({
+ accessToken,
+ defaultParams: defaultParams.deleteEvent
+ })
+ tools.push(deleteTool)
+ }
+
+ // Message tools
+ if (actions.includes('listMessages')) {
+ const listTool = new ListMessagesTool({
+ accessToken,
+ defaultParams: defaultParams.listMessages
+ })
+ tools.push(listTool)
+ }
+
+ if (actions.includes('getMessage')) {
+ const getTool = new GetMessageTool({
+ accessToken,
+ defaultParams: defaultParams.getMessage
+ })
+ tools.push(getTool)
+ }
+
+ if (actions.includes('createDraftMessage')) {
+ const createTool = new CreateDraftMessageTool({
+ accessToken,
+ defaultParams: defaultParams.createDraftMessage
+ })
+ tools.push(createTool)
+ }
+
+ if (actions.includes('sendMessage')) {
+ const sendTool = new SendMessageTool({
+ accessToken,
+ defaultParams: defaultParams.sendMessage
+ })
+ tools.push(sendTool)
+ }
+
+ if (actions.includes('updateMessage')) {
+ const updateTool = new UpdateMessageTool({
+ accessToken,
+ defaultParams: defaultParams.updateMessage
+ })
+ tools.push(updateTool)
+ }
+
+ if (actions.includes('deleteMessage')) {
+ const deleteTool = new DeleteMessageTool({
+ accessToken,
+ defaultParams: defaultParams.deleteMessage
+ })
+ tools.push(deleteTool)
+ }
+
+ if (actions.includes('copyMessage')) {
+ const copyTool = new CopyMessageTool({
+ accessToken,
+ defaultParams: defaultParams.copyMessage
+ })
+ tools.push(copyTool)
+ }
+
+ if (actions.includes('moveMessage')) {
+ const moveTool = new MoveMessageTool({
+ accessToken,
+ defaultParams: defaultParams.moveMessage
+ })
+ tools.push(moveTool)
+ }
+
+ if (actions.includes('replyMessage')) {
+ const replyTool = new ReplyMessageTool({
+ accessToken,
+ defaultParams: defaultParams.replyMessage
+ })
+ tools.push(replyTool)
+ }
+
+ if (actions.includes('forwardMessage')) {
+ const forwardTool = new ForwardMessageTool({
+ accessToken,
+ defaultParams: defaultParams.forwardMessage
+ })
+ tools.push(forwardTool)
+ }
+
+ return tools
+}
diff --git a/packages/components/nodes/tools/MicrosoftOutlook/outlook.svg b/packages/components/nodes/tools/MicrosoftOutlook/outlook.svg
new file mode 100644
index 00000000000..134a2ee9206
--- /dev/null
+++ b/packages/components/nodes/tools/MicrosoftOutlook/outlook.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/components/nodes/tools/MicrosoftTeams/MicrosoftTeams.ts b/packages/components/nodes/tools/MicrosoftTeams/MicrosoftTeams.ts
new file mode 100644
index 00000000000..d9391c50457
--- /dev/null
+++ b/packages/components/nodes/tools/MicrosoftTeams/MicrosoftTeams.ts
@@ -0,0 +1,1012 @@
+import { INode, INodeData, INodeParams } from '../../../src/Interface'
+import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils'
+import { createTeamsTools } from './core'
+
+class MicrosoftTeams_Tools implements INode {
+ label: string
+ name: string
+ version: number
+ description: string
+ type: string
+ icon: string
+ category: string
+ baseClasses: string[]
+ inputs: INodeParams[]
+ credential: INodeParams
+
+ constructor() {
+ this.label = 'Microsoft Teams'
+ this.name = 'microsoftTeams'
+ this.version = 1.0
+ this.type = 'MicrosoftTeams'
+ this.icon = 'teams.svg'
+ this.category = 'Tools'
+ this.description = 'Perform Microsoft Teams operations for channels, chats, and chat messages'
+ this.baseClasses = [this.type, 'Tool']
+ this.credential = {
+ label: 'Connect Credential',
+ name: 'credential',
+ type: 'credential',
+ credentialNames: ['microsoftTeamsOAuth2']
+ }
+ this.inputs = [
+ {
+ label: 'Type',
+ name: 'teamsType',
+ type: 'options',
+ options: [
+ {
+ label: 'Channel',
+ name: 'channel'
+ },
+ {
+ label: 'Chat',
+ name: 'chat'
+ },
+ {
+ label: 'Chat Message',
+ name: 'chatMessage'
+ }
+ ]
+ },
+ // Channel Actions
+ {
+ label: 'Channel Actions',
+ name: 'channelActions',
+ type: 'multiOptions',
+ options: [
+ {
+ label: 'List Channels',
+ name: 'listChannels'
+ },
+ {
+ label: 'Get Channel',
+ name: 'getChannel'
+ },
+ {
+ label: 'Create Channel',
+ name: 'createChannel'
+ },
+ {
+ label: 'Update Channel',
+ name: 'updateChannel'
+ },
+ {
+ label: 'Delete Channel',
+ name: 'deleteChannel'
+ },
+ {
+ label: 'Archive Channel',
+ name: 'archiveChannel'
+ },
+ {
+ label: 'Unarchive Channel',
+ name: 'unarchiveChannel'
+ },
+ {
+ label: 'List Channel Members',
+ name: 'listChannelMembers'
+ },
+ {
+ label: 'Add Channel Member',
+ name: 'addChannelMember'
+ },
+ {
+ label: 'Remove Channel Member',
+ name: 'removeChannelMember'
+ }
+ ],
+ show: {
+ teamsType: ['channel']
+ }
+ },
+ // Chat Actions
+ {
+ label: 'Chat Actions',
+ name: 'chatActions',
+ type: 'multiOptions',
+ options: [
+ {
+ label: 'List Chats',
+ name: 'listChats'
+ },
+ {
+ label: 'Get Chat',
+ name: 'getChat'
+ },
+ {
+ label: 'Create Chat',
+ name: 'createChat'
+ },
+ {
+ label: 'Update Chat',
+ name: 'updateChat'
+ },
+ {
+ label: 'Delete Chat',
+ name: 'deleteChat'
+ },
+ {
+ label: 'List Chat Members',
+ name: 'listChatMembers'
+ },
+ {
+ label: 'Add Chat Member',
+ name: 'addChatMember'
+ },
+ {
+ label: 'Remove Chat Member',
+ name: 'removeChatMember'
+ },
+ {
+ label: 'Pin Message',
+ name: 'pinMessage'
+ },
+ {
+ label: 'Unpin Message',
+ name: 'unpinMessage'
+ }
+ ],
+ show: {
+ teamsType: ['chat']
+ }
+ },
+ // Chat Message Actions
+ {
+ label: 'Chat Message Actions',
+ name: 'chatMessageActions',
+ type: 'multiOptions',
+ options: [
+ {
+ label: 'List Messages',
+ name: 'listMessages'
+ },
+ {
+ label: 'Get Message',
+ name: 'getMessage'
+ },
+ {
+ label: 'Send Message',
+ name: 'sendMessage'
+ },
+ {
+ label: 'Update Message',
+ name: 'updateMessage'
+ },
+ {
+ label: 'Delete Message',
+ name: 'deleteMessage'
+ },
+ {
+ label: 'Reply to Message',
+ name: 'replyToMessage'
+ },
+ {
+ label: 'Set Reaction',
+ name: 'setReaction'
+ },
+ {
+ label: 'Unset Reaction',
+ name: 'unsetReaction'
+ },
+ {
+ label: 'Get All Messages',
+ name: 'getAllMessages'
+ }
+ ],
+ show: {
+ teamsType: ['chatMessage']
+ }
+ },
+
+ // CHANNEL PARAMETERS
+ // List Channels Parameters
+ {
+ label: 'Team ID [List Channels]',
+ name: 'teamIdListChannels',
+ type: 'string',
+ description: 'ID of the team to list channels from',
+ show: {
+ teamsType: ['channel'],
+ channelActions: ['listChannels']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Max Results [List Channels]',
+ name: 'maxResultsListChannels',
+ type: 'number',
+ description: 'Maximum number of channels to return',
+ default: 50,
+ show: {
+ teamsType: ['channel'],
+ channelActions: ['listChannels']
+ },
+ additionalParams: true,
+ optional: true
+ },
+
+ // Get Channel Parameters
+ {
+ label: 'Team ID [Get Channel]',
+ name: 'teamIdGetChannel',
+ type: 'string',
+ description: 'ID of the team that contains the channel',
+ show: {
+ teamsType: ['channel'],
+ channelActions: ['getChannel']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Channel ID [Get Channel]',
+ name: 'channelIdGetChannel',
+ type: 'string',
+ description: 'ID of the channel to retrieve',
+ show: {
+ teamsType: ['channel'],
+ channelActions: ['getChannel']
+ },
+ additionalParams: true,
+ optional: true
+ },
+
+ // Create Channel Parameters
+ {
+ label: 'Team ID [Create Channel]',
+ name: 'teamIdCreateChannel',
+ type: 'string',
+ description: 'ID of the team to create the channel in',
+ show: {
+ teamsType: ['channel'],
+ channelActions: ['createChannel']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Display Name [Create Channel]',
+ name: 'displayNameCreateChannel',
+ type: 'string',
+ description: 'Display name of the channel',
+ placeholder: 'My New Channel',
+ show: {
+ teamsType: ['channel'],
+ channelActions: ['createChannel']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Description [Create Channel]',
+ name: 'descriptionCreateChannel',
+ type: 'string',
+ description: 'Description of the channel',
+ placeholder: 'Channel description',
+ rows: 2,
+ show: {
+ teamsType: ['channel'],
+ channelActions: ['createChannel']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Membership Type [Create Channel]',
+ name: 'membershipTypeCreateChannel',
+ type: 'options',
+ options: [
+ { label: 'Standard', name: 'standard' },
+ { label: 'Private', name: 'private' },
+ { label: 'Shared', name: 'shared' }
+ ],
+ default: 'standard',
+ description: 'Type of channel membership',
+ show: {
+ teamsType: ['channel'],
+ channelActions: ['createChannel']
+ },
+ additionalParams: true,
+ optional: true
+ },
+
+ // Update Channel Parameters
+ {
+ label: 'Team ID [Update Channel]',
+ name: 'teamIdUpdateChannel',
+ type: 'string',
+ description: 'ID of the team that contains the channel',
+ show: {
+ teamsType: ['channel'],
+ channelActions: ['updateChannel']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Channel ID [Update Channel]',
+ name: 'channelIdUpdateChannel',
+ type: 'string',
+ description: 'ID of the channel to update',
+ show: {
+ teamsType: ['channel'],
+ channelActions: ['updateChannel']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Display Name [Update Channel]',
+ name: 'displayNameUpdateChannel',
+ type: 'string',
+ description: 'New display name of the channel',
+ show: {
+ teamsType: ['channel'],
+ channelActions: ['updateChannel']
+ },
+ additionalParams: true,
+ optional: true
+ },
+
+ // Delete/Archive Channel Parameters
+ {
+ label: 'Team ID [Delete/Archive Channel]',
+ name: 'teamIdDeleteChannel',
+ type: 'string',
+ description: 'ID of the team that contains the channel',
+ show: {
+ teamsType: ['channel'],
+ channelActions: ['deleteChannel', 'archiveChannel', 'unarchiveChannel']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Channel ID [Delete/Archive Channel]',
+ name: 'channelIdDeleteChannel',
+ type: 'string',
+ description: 'ID of the channel to delete or archive',
+ show: {
+ teamsType: ['channel'],
+ channelActions: ['deleteChannel', 'archiveChannel', 'unarchiveChannel']
+ },
+ additionalParams: true,
+ optional: true
+ },
+
+ // Channel Members Parameters
+ {
+ label: 'Team ID [Channel Members]',
+ name: 'teamIdChannelMembers',
+ type: 'string',
+ description: 'ID of the team that contains the channel',
+ show: {
+ teamsType: ['channel'],
+ channelActions: ['listChannelMembers', 'addChannelMember', 'removeChannelMember']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Channel ID [Channel Members]',
+ name: 'channelIdChannelMembers',
+ type: 'string',
+ description: 'ID of the channel',
+ show: {
+ teamsType: ['channel'],
+ channelActions: ['listChannelMembers', 'addChannelMember', 'removeChannelMember']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'User ID [Add/Remove Channel Member]',
+ name: 'userIdChannelMember',
+ type: 'string',
+ description: 'ID of the user to add or remove',
+ show: {
+ teamsType: ['channel'],
+ channelActions: ['addChannelMember', 'removeChannelMember']
+ },
+ additionalParams: true,
+ optional: true
+ },
+
+ // CHAT PARAMETERS
+ // List Chats Parameters
+ {
+ label: 'Max Results [List Chats]',
+ name: 'maxResultsListChats',
+ type: 'number',
+ description: 'Maximum number of chats to return',
+ default: 50,
+ show: {
+ teamsType: ['chat'],
+ chatActions: ['listChats']
+ },
+ additionalParams: true,
+ optional: true
+ },
+
+ // Get Chat Parameters
+ {
+ label: 'Chat ID [Get Chat]',
+ name: 'chatIdGetChat',
+ type: 'string',
+ description: 'ID of the chat to retrieve',
+ show: {
+ teamsType: ['chat'],
+ chatActions: ['getChat']
+ },
+ additionalParams: true,
+ optional: true
+ },
+
+ // Create Chat Parameters
+ {
+ label: 'Chat Type [Create Chat]',
+ name: 'chatTypeCreateChat',
+ type: 'options',
+ options: [
+ { label: 'One on One', name: 'oneOnOne' },
+ { label: 'Group', name: 'group' }
+ ],
+ default: 'group',
+ description: 'Type of chat to create',
+ show: {
+ teamsType: ['chat'],
+ chatActions: ['createChat']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Topic [Create Chat]',
+ name: 'topicCreateChat',
+ type: 'string',
+ description: 'Topic/subject of the chat (for group chats)',
+ placeholder: 'Chat topic',
+ show: {
+ teamsType: ['chat'],
+ chatActions: ['createChat']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Members [Create Chat]',
+ name: 'membersCreateChat',
+ type: 'string',
+ description: 'Comma-separated list of user IDs to add to the chat',
+ placeholder: 'user1@example.com,user2@example.com',
+ show: {
+ teamsType: ['chat'],
+ chatActions: ['createChat']
+ },
+ additionalParams: true,
+ optional: true
+ },
+
+ // Update Chat Parameters
+ {
+ label: 'Chat ID [Update Chat]',
+ name: 'chatIdUpdateChat',
+ type: 'string',
+ description: 'ID of the chat to update',
+ show: {
+ teamsType: ['chat'],
+ chatActions: ['updateChat']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Topic [Update Chat]',
+ name: 'topicUpdateChat',
+ type: 'string',
+ description: 'New topic/subject of the chat',
+ show: {
+ teamsType: ['chat'],
+ chatActions: ['updateChat']
+ },
+ additionalParams: true,
+ optional: true
+ },
+
+ // Delete Chat Parameters
+ {
+ label: 'Chat ID [Delete Chat]',
+ name: 'chatIdDeleteChat',
+ type: 'string',
+ description: 'ID of the chat to delete',
+ show: {
+ teamsType: ['chat'],
+ chatActions: ['deleteChat']
+ },
+ additionalParams: true,
+ optional: true
+ },
+
+ // Chat Members Parameters
+ {
+ label: 'Chat ID [Chat Members]',
+ name: 'chatIdChatMembers',
+ type: 'string',
+ description: 'ID of the chat',
+ show: {
+ teamsType: ['chat'],
+ chatActions: ['listChatMembers', 'addChatMember', 'removeChatMember']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'User ID [Add/Remove Chat Member]',
+ name: 'userIdChatMember',
+ type: 'string',
+ description: 'ID of the user to add or remove',
+ show: {
+ teamsType: ['chat'],
+ chatActions: ['addChatMember', 'removeChatMember']
+ },
+ additionalParams: true,
+ optional: true
+ },
+
+ // Pin/Unpin Message Parameters
+ {
+ label: 'Chat ID [Pin/Unpin Message]',
+ name: 'chatIdPinMessage',
+ type: 'string',
+ description: 'ID of the chat',
+ show: {
+ teamsType: ['chat'],
+ chatActions: ['pinMessage', 'unpinMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Message ID [Pin/Unpin Message]',
+ name: 'messageIdPinMessage',
+ type: 'string',
+ description: 'ID of the message to pin or unpin',
+ show: {
+ teamsType: ['chat'],
+ chatActions: ['pinMessage', 'unpinMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+
+ // CHAT MESSAGE PARAMETERS
+ // List Messages Parameters
+ {
+ label: 'Chat/Channel ID [List Messages]',
+ name: 'chatChannelIdListMessages',
+ type: 'string',
+ description: 'ID of the chat or channel to list messages from',
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['listMessages']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Team ID [List Messages - Channel Only]',
+ name: 'teamIdListMessages',
+ type: 'string',
+ description: 'ID of the team (required for channel messages)',
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['listMessages']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Max Results [List Messages]',
+ name: 'maxResultsListMessages',
+ type: 'number',
+ description: 'Maximum number of messages to return',
+ default: 50,
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['listMessages']
+ },
+ additionalParams: true,
+ optional: true
+ },
+
+ // Get Message Parameters
+ {
+ label: 'Chat/Channel ID [Get Message]',
+ name: 'chatChannelIdGetMessage',
+ type: 'string',
+ description: 'ID of the chat or channel',
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['getMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Team ID [Get Message - Channel Only]',
+ name: 'teamIdGetMessage',
+ type: 'string',
+ description: 'ID of the team (required for channel messages)',
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['getMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Message ID [Get Message]',
+ name: 'messageIdGetMessage',
+ type: 'string',
+ description: 'ID of the message to retrieve',
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['getMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+
+ // Send Message Parameters
+ {
+ label: 'Chat/Channel ID [Send Message]',
+ name: 'chatChannelIdSendMessage',
+ type: 'string',
+ description: 'ID of the chat or channel to send message to',
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['sendMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Team ID [Send Message - Channel Only]',
+ name: 'teamIdSendMessage',
+ type: 'string',
+ description: 'ID of the team (required for channel messages)',
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['sendMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Message Body [Send Message]',
+ name: 'messageBodySendMessage',
+ type: 'string',
+ description: 'Content of the message',
+ placeholder: 'Hello, this is a message!',
+ rows: 4,
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['sendMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Content Type [Send Message]',
+ name: 'contentTypeSendMessage',
+ type: 'options',
+ options: [
+ { label: 'Text', name: 'text' },
+ { label: 'HTML', name: 'html' }
+ ],
+ default: 'text',
+ description: 'Content type of the message',
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['sendMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+
+ // Update Message Parameters
+ {
+ label: 'Chat/Channel ID [Update Message]',
+ name: 'chatChannelIdUpdateMessage',
+ type: 'string',
+ description: 'ID of the chat or channel',
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['updateMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Team ID [Update Message - Channel Only]',
+ name: 'teamIdUpdateMessage',
+ type: 'string',
+ description: 'ID of the team (required for channel messages)',
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['updateMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Message ID [Update Message]',
+ name: 'messageIdUpdateMessage',
+ type: 'string',
+ description: 'ID of the message to update',
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['updateMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+
+ // Delete Message Parameters
+ {
+ label: 'Chat/Channel ID [Delete Message]',
+ name: 'chatChannelIdDeleteMessage',
+ type: 'string',
+ description: 'ID of the chat or channel',
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['deleteMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Team ID [Delete Message - Channel Only]',
+ name: 'teamIdDeleteMessage',
+ type: 'string',
+ description: 'ID of the team (required for channel messages)',
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['deleteMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Message ID [Delete Message]',
+ name: 'messageIdDeleteMessage',
+ type: 'string',
+ description: 'ID of the message to delete',
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['deleteMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+
+ // Reply to Message Parameters
+ {
+ label: 'Chat/Channel ID [Reply to Message]',
+ name: 'chatChannelIdReplyMessage',
+ type: 'string',
+ description: 'ID of the chat or channel',
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['replyToMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Team ID [Reply to Message - Channel Only]',
+ name: 'teamIdReplyMessage',
+ type: 'string',
+ description: 'ID of the team (required for channel messages)',
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['replyToMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Message ID [Reply to Message]',
+ name: 'messageIdReplyMessage',
+ type: 'string',
+ description: 'ID of the message to reply to',
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['replyToMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Reply Body [Reply to Message]',
+ name: 'replyBodyReplyMessage',
+ type: 'string',
+ description: 'Content of the reply',
+ placeholder: 'This is my reply',
+ rows: 3,
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['replyToMessage']
+ },
+ additionalParams: true,
+ optional: true
+ },
+
+ // Set/Unset Reaction Parameters
+ {
+ label: 'Chat/Channel ID [Set/Unset Reaction]',
+ name: 'chatChannelIdReaction',
+ type: 'string',
+ description: 'ID of the chat or channel',
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['setReaction', 'unsetReaction']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Team ID [Set/Unset Reaction - Channel Only]',
+ name: 'teamIdReaction',
+ type: 'string',
+ description: 'ID of the team (required for channel messages)',
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['setReaction', 'unsetReaction']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Message ID [Set/Unset Reaction]',
+ name: 'messageIdReaction',
+ type: 'string',
+ description: 'ID of the message to react to',
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['setReaction', 'unsetReaction']
+ },
+ additionalParams: true,
+ optional: true
+ },
+ {
+ label: 'Reaction Type [Set Reaction]',
+ name: 'reactionTypeSetReaction',
+ type: 'options',
+ options: [
+ { label: 'Like', name: 'like' },
+ { label: 'Heart', name: 'heart' },
+ { label: 'Laugh', name: 'laugh' },
+ { label: 'Surprised', name: 'surprised' },
+ { label: 'Sad', name: 'sad' },
+ { label: 'Angry', name: 'angry' }
+ ],
+ default: 'like',
+ description: 'Type of reaction to set',
+ show: {
+ teamsType: ['chatMessage'],
+ chatMessageActions: ['setReaction']
+ },
+ additionalParams: true,
+ optional: true
+ }
+ ]
+ }
+
+ async init(nodeData: INodeData, _: string, options: any): Promise {
+ const teamsType = nodeData.inputs?.teamsType as string
+ const channelActions = nodeData.inputs?.channelActions as string
+ const chatActions = nodeData.inputs?.chatActions as string
+ const chatMessageActions = nodeData.inputs?.chatMessageActions as string
+
+ let actions: string[] = []
+ if (teamsType === 'channel') {
+ actions = convertMultiOptionsToStringArray(channelActions)
+ } else if (teamsType === 'chat') {
+ actions = convertMultiOptionsToStringArray(chatActions)
+ } else if (teamsType === 'chatMessage') {
+ actions = convertMultiOptionsToStringArray(chatMessageActions)
+ }
+
+ let credentialData = await getCredentialData(nodeData.credential ?? '', options)
+ credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options)
+ const accessToken = getCredentialParam('access_token', credentialData, nodeData)
+
+ if (!accessToken) {
+ throw new Error('No access token found in credential')
+ }
+
+ const defaultParams = this.transformNodeInputsToToolArgs(nodeData)
+
+ const teamsTools = createTeamsTools({
+ accessToken,
+ actions,
+ defaultParams,
+ type: teamsType
+ })
+
+ return teamsTools
+ }
+
+ transformNodeInputsToToolArgs(nodeData: INodeData): Record {
+ // Collect default parameters from inputs
+ const defaultParams: Record = {}
+
+ // Channel parameters
+ if (nodeData.inputs?.teamIdListChannels) defaultParams.teamIdListChannels = nodeData.inputs.teamIdListChannels
+ if (nodeData.inputs?.maxResultsListChannels) defaultParams.maxResultsListChannels = nodeData.inputs.maxResultsListChannels
+ if (nodeData.inputs?.teamIdGetChannel) defaultParams.teamIdGetChannel = nodeData.inputs.teamIdGetChannel
+ if (nodeData.inputs?.channelIdGetChannel) defaultParams.channelIdGetChannel = nodeData.inputs.channelIdGetChannel
+ if (nodeData.inputs?.teamIdCreateChannel) defaultParams.teamIdCreateChannel = nodeData.inputs.teamIdCreateChannel
+ if (nodeData.inputs?.displayNameCreateChannel) defaultParams.displayNameCreateChannel = nodeData.inputs.displayNameCreateChannel
+ if (nodeData.inputs?.descriptionCreateChannel) defaultParams.descriptionCreateChannel = nodeData.inputs.descriptionCreateChannel
+ if (nodeData.inputs?.membershipTypeCreateChannel)
+ defaultParams.membershipTypeCreateChannel = nodeData.inputs.membershipTypeCreateChannel
+ if (nodeData.inputs?.teamIdUpdateChannel) defaultParams.teamIdUpdateChannel = nodeData.inputs.teamIdUpdateChannel
+ if (nodeData.inputs?.channelIdUpdateChannel) defaultParams.channelIdUpdateChannel = nodeData.inputs.channelIdUpdateChannel
+ if (nodeData.inputs?.displayNameUpdateChannel) defaultParams.displayNameUpdateChannel = nodeData.inputs.displayNameUpdateChannel
+ if (nodeData.inputs?.teamIdDeleteChannel) defaultParams.teamIdDeleteChannel = nodeData.inputs.teamIdDeleteChannel
+ if (nodeData.inputs?.channelIdDeleteChannel) defaultParams.channelIdDeleteChannel = nodeData.inputs.channelIdDeleteChannel
+ if (nodeData.inputs?.teamIdChannelMembers) defaultParams.teamIdChannelMembers = nodeData.inputs.teamIdChannelMembers
+ if (nodeData.inputs?.channelIdChannelMembers) defaultParams.channelIdChannelMembers = nodeData.inputs.channelIdChannelMembers
+ if (nodeData.inputs?.userIdChannelMember) defaultParams.userIdChannelMember = nodeData.inputs.userIdChannelMember
+
+ // Chat parameters
+ if (nodeData.inputs?.maxResultsListChats) defaultParams.maxResultsListChats = nodeData.inputs.maxResultsListChats
+ if (nodeData.inputs?.chatIdGetChat) defaultParams.chatIdGetChat = nodeData.inputs.chatIdGetChat
+ if (nodeData.inputs?.chatTypeCreateChat) defaultParams.chatTypeCreateChat = nodeData.inputs.chatTypeCreateChat
+ if (nodeData.inputs?.topicCreateChat) defaultParams.topicCreateChat = nodeData.inputs.topicCreateChat
+ if (nodeData.inputs?.membersCreateChat) defaultParams.membersCreateChat = nodeData.inputs.membersCreateChat
+ if (nodeData.inputs?.chatIdUpdateChat) defaultParams.chatIdUpdateChat = nodeData.inputs.chatIdUpdateChat
+ if (nodeData.inputs?.topicUpdateChat) defaultParams.topicUpdateChat = nodeData.inputs.topicUpdateChat
+ if (nodeData.inputs?.chatIdDeleteChat) defaultParams.chatIdDeleteChat = nodeData.inputs.chatIdDeleteChat
+ if (nodeData.inputs?.chatIdChatMembers) defaultParams.chatIdChatMembers = nodeData.inputs.chatIdChatMembers
+ if (nodeData.inputs?.userIdChatMember) defaultParams.userIdChatMember = nodeData.inputs.userIdChatMember
+ if (nodeData.inputs?.chatIdPinMessage) defaultParams.chatIdPinMessage = nodeData.inputs.chatIdPinMessage
+ if (nodeData.inputs?.messageIdPinMessage) defaultParams.messageIdPinMessage = nodeData.inputs.messageIdPinMessage
+
+ // Chat Message parameters
+ if (nodeData.inputs?.chatChannelIdListMessages) defaultParams.chatChannelIdListMessages = nodeData.inputs.chatChannelIdListMessages
+ if (nodeData.inputs?.teamIdListMessages) defaultParams.teamIdListMessages = nodeData.inputs.teamIdListMessages
+ if (nodeData.inputs?.maxResultsListMessages) defaultParams.maxResultsListMessages = nodeData.inputs.maxResultsListMessages
+ if (nodeData.inputs?.chatChannelIdGetMessage) defaultParams.chatChannelIdGetMessage = nodeData.inputs.chatChannelIdGetMessage
+ if (nodeData.inputs?.teamIdGetMessage) defaultParams.teamIdGetMessage = nodeData.inputs.teamIdGetMessage
+ if (nodeData.inputs?.messageIdGetMessage) defaultParams.messageIdGetMessage = nodeData.inputs.messageIdGetMessage
+ if (nodeData.inputs?.chatChannelIdSendMessage) defaultParams.chatChannelIdSendMessage = nodeData.inputs.chatChannelIdSendMessage
+ if (nodeData.inputs?.teamIdSendMessage) defaultParams.teamIdSendMessage = nodeData.inputs.teamIdSendMessage
+ if (nodeData.inputs?.messageBodySendMessage) defaultParams.messageBodySendMessage = nodeData.inputs.messageBodySendMessage
+ if (nodeData.inputs?.contentTypeSendMessage) defaultParams.contentTypeSendMessage = nodeData.inputs.contentTypeSendMessage
+ if (nodeData.inputs?.chatChannelIdUpdateMessage)
+ defaultParams.chatChannelIdUpdateMessage = nodeData.inputs.chatChannelIdUpdateMessage
+ if (nodeData.inputs?.teamIdUpdateMessage) defaultParams.teamIdUpdateMessage = nodeData.inputs.teamIdUpdateMessage
+ if (nodeData.inputs?.messageIdUpdateMessage) defaultParams.messageIdUpdateMessage = nodeData.inputs.messageIdUpdateMessage
+ if (nodeData.inputs?.chatChannelIdDeleteMessage)
+ defaultParams.chatChannelIdDeleteMessage = nodeData.inputs.chatChannelIdDeleteMessage
+ if (nodeData.inputs?.teamIdDeleteMessage) defaultParams.teamIdDeleteMessage = nodeData.inputs.teamIdDeleteMessage
+ if (nodeData.inputs?.messageIdDeleteMessage) defaultParams.messageIdDeleteMessage = nodeData.inputs.messageIdDeleteMessage
+ if (nodeData.inputs?.chatChannelIdReplyMessage) defaultParams.chatChannelIdReplyMessage = nodeData.inputs.chatChannelIdReplyMessage
+ if (nodeData.inputs?.teamIdReplyMessage) defaultParams.teamIdReplyMessage = nodeData.inputs.teamIdReplyMessage
+ if (nodeData.inputs?.messageIdReplyMessage) defaultParams.messageIdReplyMessage = nodeData.inputs.messageIdReplyMessage
+ if (nodeData.inputs?.replyBodyReplyMessage) defaultParams.replyBodyReplyMessage = nodeData.inputs.replyBodyReplyMessage
+ if (nodeData.inputs?.chatChannelIdReaction) defaultParams.chatChannelIdReaction = nodeData.inputs.chatChannelIdReaction
+ if (nodeData.inputs?.teamIdReaction) defaultParams.teamIdReaction = nodeData.inputs.teamIdReaction
+ if (nodeData.inputs?.messageIdReaction) defaultParams.messageIdReaction = nodeData.inputs.messageIdReaction
+ if (nodeData.inputs?.reactionTypeSetReaction) defaultParams.reactionTypeSetReaction = nodeData.inputs.reactionTypeSetReaction
+
+ return defaultParams
+ }
+}
+
+module.exports = { nodeClass: MicrosoftTeams_Tools }
diff --git a/packages/components/nodes/tools/MicrosoftTeams/core.ts b/packages/components/nodes/tools/MicrosoftTeams/core.ts
new file mode 100644
index 00000000000..a0c08091469
--- /dev/null
+++ b/packages/components/nodes/tools/MicrosoftTeams/core.ts
@@ -0,0 +1,1756 @@
+import { z } from 'zod'
+import { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager'
+import { DynamicStructuredTool, DynamicStructuredToolInput } from '../OpenAPIToolkit/core'
+import { TOOL_ARGS_PREFIX } from '../../../src/agents'
+
+interface TeamsToolOptions {
+ accessToken: string
+ actions: string[]
+ defaultParams: any
+ type: string
+}
+
+const BASE_URL = 'https://graph.microsoft.com/v1.0'
+
+// Helper function to make Graph API requests
+async function makeGraphRequest(
+ endpoint: string,
+ method: 'GET' | 'POST' | 'PATCH' | 'DELETE' = 'GET',
+ body?: any,
+ accessToken?: string
+): Promise {
+ const headers: Record = {
+ Authorization: `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json'
+ }
+
+ const config: RequestInit = {
+ method,
+ headers
+ }
+
+ if (body && (method === 'POST' || method === 'PATCH')) {
+ config.body = JSON.stringify(body)
+ }
+
+ try {
+ const response = await fetch(`${BASE_URL}${endpoint}`, config)
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ throw new Error(`Microsoft Graph API error: ${response.status} ${response.statusText} - ${errorText}`)
+ }
+
+ // Handle empty responses for DELETE operations
+ if (method === 'DELETE' || response.status === 204) {
+ return { success: true, message: 'Operation completed successfully' }
+ }
+
+ return await response.json()
+ } catch (error) {
+ throw new Error(`Microsoft Graph request failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
+ }
+}
+
+// Base Teams Tool class
+abstract class BaseTeamsTool extends DynamicStructuredTool {
+ accessToken = ''
+ protected defaultParams: any
+
+ constructor(args: DynamicStructuredToolInput & { accessToken?: string; defaultParams?: any }) {
+ super(args)
+ this.accessToken = args.accessToken ?? ''
+ this.defaultParams = args.defaultParams || {}
+ }
+
+ protected async makeTeamsRequest(endpoint: string, method: string = 'GET', body?: any) {
+ return await makeGraphRequest(endpoint, method as any, body, this.accessToken)
+ }
+
+ protected formatResponse(data: any, params: any): string {
+ return JSON.stringify(data) + TOOL_ARGS_PREFIX + JSON.stringify(params)
+ }
+
+ // Abstract method that must be implemented by subclasses
+ protected abstract _call(arg: any, runManager?: CallbackManagerForToolRun, parentConfig?: any): Promise
+}
+
+// CHANNEL TOOLS
+
+class ListChannelsTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'list_channels',
+ description: 'List all channels in a team',
+ schema: z.object({
+ teamId: z.string().describe('ID of the team to list channels from'),
+ maxResults: z.number().optional().default(50).describe('Maximum number of channels to return')
+ }),
+ baseUrl: BASE_URL,
+ method: 'GET',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { teamId, maxResults = 50 } = params
+
+ if (!teamId) {
+ throw new Error('Team ID is required to list channels')
+ }
+
+ try {
+ const endpoint = `/teams/${teamId}/channels`
+ const result = await this.makeTeamsRequest(endpoint)
+
+ // Filter results to maxResults on client side since $top is not supported
+ const channels = result.value || []
+ const limitedChannels = channels.slice(0, maxResults)
+
+ const responseData = {
+ success: true,
+ channels: limitedChannels,
+ count: limitedChannels.length,
+ total: channels.length
+ }
+
+ return this.formatResponse(responseData, params)
+ } catch (error) {
+ return `Error listing channels: ${error}`
+ }
+ }
+}
+
+class GetChannelTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'get_channel',
+ description: 'Get details of a specific channel',
+ schema: z.object({
+ teamId: z.string().describe('ID of the team that contains the channel'),
+ channelId: z.string().describe('ID of the channel to retrieve')
+ }),
+ baseUrl: BASE_URL,
+ method: 'GET',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { teamId, channelId } = params
+
+ if (!teamId || !channelId) {
+ throw new Error('Both Team ID and Channel ID are required')
+ }
+
+ try {
+ const endpoint = `/teams/${teamId}/channels/${channelId}`
+ const result = await this.makeTeamsRequest(endpoint)
+
+ return this.formatResponse(
+ {
+ success: true,
+ channel: result
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error getting channel: ${error}`, params)
+ }
+ }
+}
+
+class CreateChannelTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'create_channel',
+ description: 'Create a new channel in a team',
+ schema: z.object({
+ teamId: z.string().describe('ID of the team to create the channel in'),
+ displayName: z.string().describe('Display name of the channel'),
+ description: z.string().optional().describe('Description of the channel'),
+ membershipType: z
+ .enum(['standard', 'private', 'shared'])
+ .optional()
+ .default('standard')
+ .describe('Type of channel membership')
+ }),
+ baseUrl: BASE_URL,
+ method: 'POST',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { teamId, displayName, description, membershipType = 'standard' } = params
+
+ if (!teamId || !displayName) {
+ throw new Error('Team ID and Display Name are required to create a channel')
+ }
+
+ try {
+ const body = {
+ displayName,
+ membershipType,
+ ...(description && { description })
+ }
+
+ const endpoint = `/teams/${teamId}/channels`
+ const result = await this.makeTeamsRequest(endpoint, 'POST', body)
+
+ return this.formatResponse(
+ {
+ success: true,
+ channel: result,
+ message: `Channel "${displayName}" created successfully`
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error creating channel: ${error}`, params)
+ }
+ }
+}
+
+class UpdateChannelTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'update_channel',
+ description: 'Update an existing channel',
+ schema: z.object({
+ teamId: z.string().describe('ID of the team that contains the channel'),
+ channelId: z.string().describe('ID of the channel to update'),
+ displayName: z.string().optional().describe('New display name of the channel'),
+ description: z.string().optional().describe('New description of the channel')
+ }),
+ baseUrl: BASE_URL,
+ method: 'PATCH',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { teamId, channelId, displayName, description } = params
+
+ if (!teamId || !channelId) {
+ throw new Error('Both Team ID and Channel ID are required')
+ }
+
+ try {
+ const body: any = {}
+ if (displayName) body.displayName = displayName
+ if (description) body.description = description
+
+ if (Object.keys(body).length === 0) {
+ throw new Error('At least one field to update must be provided')
+ }
+
+ const endpoint = `/teams/${teamId}/channels/${channelId}`
+ await this.makeTeamsRequest(endpoint, 'PATCH', body)
+
+ return this.formatResponse(
+ {
+ success: true,
+ message: 'Channel updated successfully'
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error updating channel: ${error}`, params)
+ }
+ }
+}
+
+class DeleteChannelTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'delete_channel',
+ description: 'Delete a channel from a team',
+ schema: z.object({
+ teamId: z.string().describe('ID of the team that contains the channel'),
+ channelId: z.string().describe('ID of the channel to delete')
+ }),
+ baseUrl: BASE_URL,
+ method: 'DELETE',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { teamId, channelId } = params
+
+ if (!teamId || !channelId) {
+ throw new Error('Both Team ID and Channel ID are required')
+ }
+
+ try {
+ const endpoint = `/teams/${teamId}/channels/${channelId}`
+ await this.makeTeamsRequest(endpoint, 'DELETE')
+
+ return this.formatResponse(
+ {
+ success: true,
+ message: 'Channel deleted successfully'
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error deleting channel: ${error}`, params)
+ }
+ }
+}
+
+class ArchiveChannelTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'archive_channel',
+ description: 'Archive a channel in a team',
+ schema: z.object({
+ teamId: z.string().describe('ID of the team that contains the channel'),
+ channelId: z.string().describe('ID of the channel to archive')
+ }),
+ baseUrl: BASE_URL,
+ method: 'POST',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { teamId, channelId } = params
+
+ if (!teamId || !channelId) {
+ throw new Error('Both Team ID and Channel ID are required')
+ }
+
+ try {
+ const endpoint = `/teams/${teamId}/channels/${channelId}/archive`
+ await this.makeTeamsRequest(endpoint, 'POST', {})
+
+ return this.formatResponse(
+ {
+ success: true,
+ message: 'Channel archived successfully'
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error archiving channel: ${error}`, params)
+ }
+ }
+}
+
+class UnarchiveChannelTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'unarchive_channel',
+ description: 'Unarchive a channel in a team',
+ schema: z.object({
+ teamId: z.string().describe('ID of the team that contains the channel'),
+ channelId: z.string().describe('ID of the channel to unarchive')
+ }),
+ baseUrl: BASE_URL,
+ method: 'POST',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { teamId, channelId } = params
+
+ if (!teamId || !channelId) {
+ throw new Error('Both Team ID and Channel ID are required')
+ }
+
+ try {
+ const endpoint = `/teams/${teamId}/channels/${channelId}/unarchive`
+ await this.makeTeamsRequest(endpoint, 'POST', {})
+
+ return this.formatResponse(
+ {
+ success: true,
+ message: 'Channel unarchived successfully'
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error unarchiving channel: ${error}`, params)
+ }
+ }
+}
+
+class ListChannelMembersTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'list_channel_members',
+ description: 'List members of a channel',
+ schema: z.object({
+ teamId: z.string().describe('ID of the team that contains the channel'),
+ channelId: z.string().describe('ID of the channel')
+ }),
+ baseUrl: BASE_URL,
+ method: 'GET',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { teamId, channelId } = params
+
+ if (!teamId || !channelId) {
+ throw new Error('Both Team ID and Channel ID are required')
+ }
+
+ try {
+ const endpoint = `/teams/${teamId}/channels/${channelId}/members`
+ const result = await this.makeTeamsRequest(endpoint)
+
+ return this.formatResponse(
+ {
+ success: true,
+ members: result.value || [],
+ count: result.value?.length || 0
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error listing channel members: ${error}`, params)
+ }
+ }
+}
+
+class AddChannelMemberTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'add_channel_member',
+ description: 'Add a member to a channel',
+ schema: z.object({
+ teamId: z.string().describe('ID of the team that contains the channel'),
+ channelId: z.string().describe('ID of the channel'),
+ userId: z.string().describe('ID of the user to add')
+ }),
+ baseUrl: BASE_URL,
+ method: 'POST',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { teamId, channelId, userId } = params
+
+ if (!teamId || !channelId || !userId) {
+ throw new Error('Team ID, Channel ID, and User ID are all required')
+ }
+
+ try {
+ const body = {
+ '@odata.type': '#microsoft.graph.aadUserConversationMember',
+ 'user@odata.bind': `https://graph.microsoft.com/v1.0/users('${userId}')`
+ }
+
+ const endpoint = `/teams/${teamId}/channels/${channelId}/members`
+ await this.makeTeamsRequest(endpoint, 'POST', body)
+
+ return this.formatResponse(
+ {
+ success: true,
+ message: 'Member added to channel successfully'
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error adding channel member: ${error}`, params)
+ }
+ }
+}
+
+class RemoveChannelMemberTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'remove_channel_member',
+ description: 'Remove a member from a channel',
+ schema: z.object({
+ teamId: z.string().describe('ID of the team that contains the channel'),
+ channelId: z.string().describe('ID of the channel'),
+ userId: z.string().describe('ID of the user to remove')
+ }),
+ baseUrl: BASE_URL,
+ method: 'DELETE',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { teamId, channelId, userId } = params
+
+ if (!teamId || !channelId || !userId) {
+ throw new Error('Team ID, Channel ID, and User ID are all required')
+ }
+
+ try {
+ // First get the membership ID
+ const membersEndpoint = `/teams/${teamId}/channels/${channelId}/members`
+ const membersResult = await this.makeTeamsRequest(membersEndpoint)
+
+ const member = membersResult.value?.find((m: any) => m.userId === userId)
+ if (!member) {
+ throw new Error('User is not a member of this channel')
+ }
+
+ const endpoint = `/teams/${teamId}/channels/${channelId}/members/${member.id}`
+ await this.makeTeamsRequest(endpoint, 'DELETE')
+
+ return this.formatResponse(
+ {
+ success: true,
+ message: 'Member removed from channel successfully'
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error removing channel member: ${error}`, params)
+ }
+ }
+}
+
+// CHAT TOOLS
+
+class ListChatsTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'list_chats',
+ description: 'List all chats for the current user',
+ schema: z.object({
+ maxResults: z.number().optional().default(50).describe('Maximum number of chats to return')
+ }),
+ baseUrl: BASE_URL,
+ method: 'GET',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { maxResults = 50 } = params
+
+ try {
+ const endpoint = `/me/chats?$top=${maxResults}`
+ const result = await this.makeTeamsRequest(endpoint)
+
+ return this.formatResponse(
+ {
+ success: true,
+ chats: result.value || [],
+ count: result.value?.length || 0
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error listing chats: ${error}`, params)
+ }
+ }
+}
+
+class GetChatTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'get_chat',
+ description: 'Get details of a specific chat',
+ schema: z.object({
+ chatId: z.string().describe('ID of the chat to retrieve')
+ }),
+ baseUrl: BASE_URL,
+ method: 'GET',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { chatId } = params
+
+ if (!chatId) {
+ throw new Error('Chat ID is required')
+ }
+
+ try {
+ const endpoint = `/chats/${chatId}`
+ const result = await this.makeTeamsRequest(endpoint)
+
+ return this.formatResponse(
+ {
+ success: true,
+ chat: result
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error getting chat: ${error}`, params)
+ }
+ }
+}
+
+class CreateChatTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'create_chat',
+ description: 'Create a new chat',
+ schema: z.object({
+ chatType: z.enum(['oneOnOne', 'group']).optional().default('group').describe('Type of chat to create'),
+ topic: z.string().optional().describe('Topic/subject of the chat (for group chats)'),
+ members: z.string().describe('Comma-separated list of user IDs to add to the chat')
+ }),
+ baseUrl: BASE_URL,
+ method: 'POST',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { chatType = 'group', topic, members } = params
+
+ if (!members) {
+ throw new Error('Members list is required to create a chat')
+ }
+
+ try {
+ const memberIds = members.split(',').map((id: string) => id.trim())
+ const chatMembers = memberIds.map((userId: string) => ({
+ '@odata.type': '#microsoft.graph.aadUserConversationMember',
+ 'user@odata.bind': `https://graph.microsoft.com/v1.0/users('${userId}')`
+ }))
+
+ const body: any = {
+ chatType,
+ members: chatMembers
+ }
+
+ if (topic && chatType === 'group') {
+ body.topic = topic
+ }
+
+ const endpoint = '/chats'
+ const result = await this.makeTeamsRequest(endpoint, 'POST', body)
+
+ return this.formatResponse(
+ {
+ success: true,
+ chat: result,
+ message: 'Chat created successfully'
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error creating chat: ${error}`, params)
+ }
+ }
+}
+
+class UpdateChatTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'update_chat',
+ description: 'Update an existing chat',
+ schema: z.object({
+ chatId: z.string().describe('ID of the chat to update'),
+ topic: z.string().describe('New topic/subject of the chat')
+ }),
+ baseUrl: BASE_URL,
+ method: 'PATCH',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { chatId, topic } = params
+
+ if (!chatId) {
+ throw new Error('Chat ID is required')
+ }
+
+ if (!topic) {
+ throw new Error('Topic is required to update a chat')
+ }
+
+ try {
+ const body = { topic }
+ const endpoint = `/chats/${chatId}`
+ await this.makeTeamsRequest(endpoint, 'PATCH', body)
+
+ return this.formatResponse(
+ {
+ success: true,
+ message: 'Chat updated successfully'
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error updating chat: ${error}`, params)
+ }
+ }
+}
+
+class DeleteChatTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'delete_chat',
+ description: 'Delete a chat',
+ schema: z.object({
+ chatId: z.string().describe('ID of the chat to delete')
+ }),
+ baseUrl: BASE_URL,
+ method: 'DELETE',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { chatId } = params
+
+ if (!chatId) {
+ throw new Error('Chat ID is required')
+ }
+
+ try {
+ const endpoint = `/chats/${chatId}`
+ await this.makeTeamsRequest(endpoint, 'DELETE')
+
+ return this.formatResponse(
+ {
+ success: true,
+ message: 'Chat deleted successfully'
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error deleting chat: ${error}`, params)
+ }
+ }
+}
+
+class ListChatMembersTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'list_chat_members',
+ description: 'List members of a chat',
+ schema: z.object({
+ chatId: z.string().describe('ID of the chat')
+ }),
+ baseUrl: BASE_URL,
+ method: 'GET',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { chatId } = params
+
+ if (!chatId) {
+ throw new Error('Chat ID is required')
+ }
+
+ try {
+ const endpoint = `/chats/${chatId}/members`
+ const result = await this.makeTeamsRequest(endpoint)
+
+ return this.formatResponse(
+ {
+ success: true,
+ members: result.value || [],
+ count: result.value?.length || 0
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error listing chat members: ${error}`, params)
+ }
+ }
+}
+
+class AddChatMemberTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'add_chat_member',
+ description: 'Add a member to a chat',
+ schema: z.object({
+ chatId: z.string().describe('ID of the chat'),
+ userId: z.string().describe('ID of the user to add')
+ }),
+ baseUrl: BASE_URL,
+ method: 'POST',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { chatId, userId } = params
+
+ if (!chatId || !userId) {
+ throw new Error('Both Chat ID and User ID are required')
+ }
+
+ try {
+ const body = {
+ '@odata.type': '#microsoft.graph.aadUserConversationMember',
+ 'user@odata.bind': `https://graph.microsoft.com/v1.0/users('${userId}')`
+ }
+
+ const endpoint = `/chats/${chatId}/members`
+ await this.makeTeamsRequest(endpoint, 'POST', body)
+
+ return this.formatResponse(
+ {
+ success: true,
+ message: 'Member added to chat successfully'
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error adding chat member: ${error}`, params)
+ }
+ }
+}
+
+class RemoveChatMemberTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'remove_chat_member',
+ description: 'Remove a member from a chat',
+ schema: z.object({
+ chatId: z.string().describe('ID of the chat'),
+ userId: z.string().describe('ID of the user to remove')
+ }),
+ baseUrl: BASE_URL,
+ method: 'DELETE',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { chatId, userId } = params
+
+ if (!chatId || !userId) {
+ throw new Error('Both Chat ID and User ID are required')
+ }
+
+ try {
+ // First get the membership ID
+ const membersEndpoint = `/chats/${chatId}/members`
+ const membersResult = await this.makeTeamsRequest(membersEndpoint)
+
+ const member = membersResult.value?.find((m: any) => m.userId === userId)
+ if (!member) {
+ throw new Error('User is not a member of this chat')
+ }
+
+ const endpoint = `/chats/${chatId}/members/${member.id}`
+ await this.makeTeamsRequest(endpoint, 'DELETE')
+
+ return this.formatResponse(
+ {
+ success: true,
+ message: 'Member removed from chat successfully'
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error removing chat member: ${error}`, params)
+ }
+ }
+}
+
+class PinMessageTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'pin_message',
+ description: 'Pin a message in a chat',
+ schema: z.object({
+ chatId: z.string().describe('ID of the chat'),
+ messageId: z.string().describe('ID of the message to pin')
+ }),
+ baseUrl: BASE_URL,
+ method: 'POST',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { chatId, messageId } = params
+
+ if (!chatId || !messageId) {
+ throw new Error('Both Chat ID and Message ID are required')
+ }
+
+ try {
+ const body = {
+ message: {
+ '@odata.bind': `https://graph.microsoft.com/v1.0/chats('${chatId}')/messages('${messageId}')`
+ }
+ }
+
+ const endpoint = `/chats/${chatId}/pinnedMessages`
+ await this.makeTeamsRequest(endpoint, 'POST', body)
+
+ return this.formatResponse(
+ {
+ success: true,
+ message: 'Message pinned successfully'
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error pinning message: ${error}`, params)
+ }
+ }
+}
+
+class UnpinMessageTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'unpin_message',
+ description: 'Unpin a message from a chat',
+ schema: z.object({
+ chatId: z.string().describe('ID of the chat'),
+ messageId: z.string().describe('ID of the message to unpin')
+ }),
+ baseUrl: BASE_URL,
+ method: 'DELETE',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { chatId, messageId } = params
+
+ if (!chatId || !messageId) {
+ throw new Error('Both Chat ID and Message ID are required')
+ }
+
+ try {
+ // First get the pinned messages to find the pinned message ID
+ const pinnedEndpoint = `/chats/${chatId}/pinnedMessages`
+ const pinnedResult = await this.makeTeamsRequest(pinnedEndpoint)
+
+ const pinnedMessage = pinnedResult.value?.find((pm: any) => pm.message?.id === messageId)
+ if (!pinnedMessage) {
+ throw new Error('Message is not pinned in this chat')
+ }
+
+ const endpoint = `/chats/${chatId}/pinnedMessages/${pinnedMessage.id}`
+ await this.makeTeamsRequest(endpoint, 'DELETE')
+
+ return this.formatResponse(
+ {
+ success: true,
+ message: 'Message unpinned successfully'
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error unpinning message: ${error}`, params)
+ }
+ }
+}
+
+// CHAT MESSAGE TOOLS
+
+class ListMessagesTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'list_messages',
+ description: 'List messages in a chat or channel',
+ schema: z.object({
+ chatChannelId: z.string().describe('ID of the chat or channel to list messages from'),
+ teamId: z.string().optional().describe('ID of the team (required for channel messages)'),
+ maxResults: z.number().optional().default(50).describe('Maximum number of messages to return')
+ }),
+ baseUrl: BASE_URL,
+ method: 'GET',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { chatChannelId, teamId, maxResults = 50 } = params
+
+ if (!chatChannelId) {
+ throw new Error('Chat or Channel ID is required')
+ }
+
+ try {
+ let endpoint: string
+ if (teamId) {
+ // Channel messages
+ endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages?$top=${maxResults}`
+ } else {
+ // Chat messages
+ endpoint = `/chats/${chatChannelId}/messages?$top=${maxResults}`
+ }
+
+ const result = await this.makeTeamsRequest(endpoint)
+
+ return this.formatResponse(
+ {
+ success: true,
+ messages: result.value || [],
+ count: result.value?.length || 0,
+ context: teamId ? 'channel' : 'chat'
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error listing messages: ${error}`, params)
+ }
+ }
+}
+
+class GetMessageTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'get_message',
+ description: 'Get details of a specific message',
+ schema: z.object({
+ chatChannelId: z.string().describe('ID of the chat or channel'),
+ teamId: z.string().optional().describe('ID of the team (required for channel messages)'),
+ messageId: z.string().describe('ID of the message to retrieve')
+ }),
+ baseUrl: BASE_URL,
+ method: 'GET',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { chatChannelId, teamId, messageId } = params
+
+ if (!chatChannelId || !messageId) {
+ throw new Error('Chat/Channel ID and Message ID are required')
+ }
+
+ try {
+ let endpoint: string
+ if (teamId) {
+ // Channel message
+ endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages/${messageId}`
+ } else {
+ // Chat message
+ endpoint = `/chats/${chatChannelId}/messages/${messageId}`
+ }
+
+ const result = await this.makeTeamsRequest(endpoint)
+
+ return this.formatResponse(
+ {
+ success: true,
+ message: result,
+ context: teamId ? 'channel' : 'chat'
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error getting message: ${error}`, params)
+ }
+ }
+}
+
+class SendMessageTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'send_message',
+ description: 'Send a message to a chat or channel',
+ schema: z.object({
+ chatChannelId: z.string().describe('ID of the chat or channel to send message to'),
+ teamId: z.string().optional().describe('ID of the team (required for channel messages)'),
+ messageBody: z.string().describe('Content of the message'),
+ contentType: z.enum(['text', 'html']).optional().default('text').describe('Content type of the message')
+ }),
+ baseUrl: BASE_URL,
+ method: 'POST',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { chatChannelId, teamId, messageBody, contentType = 'text' } = params
+
+ if (!chatChannelId || !messageBody) {
+ throw new Error('Chat/Channel ID and Message Body are required')
+ }
+
+ try {
+ const body = {
+ body: {
+ contentType,
+ content: messageBody
+ }
+ }
+
+ let endpoint: string
+ if (teamId) {
+ // Channel message
+ endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages`
+ } else {
+ // Chat message
+ endpoint = `/chats/${chatChannelId}/messages`
+ }
+
+ const result = await this.makeTeamsRequest(endpoint, 'POST', body)
+
+ return this.formatResponse(
+ {
+ success: true,
+ message: result,
+ context: teamId ? 'channel' : 'chat',
+ messageText: 'Message sent successfully'
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error sending message: ${error}`, params)
+ }
+ }
+}
+
+class UpdateMessageTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'update_message',
+ description: 'Update an existing message',
+ schema: z.object({
+ chatChannelId: z.string().describe('ID of the chat or channel'),
+ teamId: z.string().optional().describe('ID of the team (required for channel messages)'),
+ messageId: z.string().describe('ID of the message to update')
+ }),
+ baseUrl: BASE_URL,
+ method: 'PATCH',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { chatChannelId, teamId, messageId } = params
+
+ if (!chatChannelId || !messageId) {
+ throw new Error('Chat/Channel ID and Message ID are required')
+ }
+
+ try {
+ // Note: Message update is primarily for policy violations in Teams
+ const body = {
+ policyViolation: null
+ }
+
+ let endpoint: string
+ if (teamId) {
+ // Channel message
+ endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages/${messageId}`
+ } else {
+ // Chat message
+ endpoint = `/chats/${chatChannelId}/messages/${messageId}`
+ }
+
+ await this.makeTeamsRequest(endpoint, 'PATCH', body)
+
+ return this.formatResponse(
+ {
+ success: true,
+ message: 'Message updated successfully',
+ context: teamId ? 'channel' : 'chat'
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error updating message: ${error}`, params)
+ }
+ }
+}
+
+class DeleteMessageTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'delete_message',
+ description: 'Delete a message',
+ schema: z.object({
+ chatChannelId: z.string().describe('ID of the chat or channel'),
+ teamId: z.string().optional().describe('ID of the team (required for channel messages)'),
+ messageId: z.string().describe('ID of the message to delete')
+ }),
+ baseUrl: BASE_URL,
+ method: 'DELETE',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { chatChannelId, teamId, messageId } = params
+
+ if (!chatChannelId || !messageId) {
+ throw new Error('Chat/Channel ID and Message ID are required')
+ }
+
+ try {
+ let endpoint: string
+ if (teamId) {
+ // Channel message - use soft delete
+ endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages/${messageId}/softDelete`
+ } else {
+ // Chat message - use soft delete
+ endpoint = `/chats/${chatChannelId}/messages/${messageId}/softDelete`
+ }
+
+ await this.makeTeamsRequest(endpoint, 'POST', {})
+
+ return this.formatResponse(
+ {
+ success: true,
+ message: 'Message deleted successfully',
+ context: teamId ? 'channel' : 'chat'
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error deleting message: ${error}`, params)
+ }
+ }
+}
+
+class ReplyToMessageTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'reply_to_message',
+ description: 'Reply to a message in a chat or channel',
+ schema: z.object({
+ chatChannelId: z.string().describe('ID of the chat or channel'),
+ teamId: z.string().optional().describe('ID of the team (required for channel messages)'),
+ messageId: z.string().describe('ID of the message to reply to'),
+ replyBody: z.string().describe('Content of the reply'),
+ contentType: z.enum(['text', 'html']).optional().default('text').describe('Content type of the reply')
+ }),
+ baseUrl: BASE_URL,
+ method: 'POST',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { chatChannelId, teamId, messageId, replyBody, contentType = 'text' } = params
+
+ if (!chatChannelId || !messageId || !replyBody) {
+ throw new Error('Chat/Channel ID, Message ID, and Reply Body are required')
+ }
+
+ try {
+ const body = {
+ body: {
+ contentType,
+ content: replyBody
+ }
+ }
+
+ let endpoint: string
+ if (teamId) {
+ // Channel message reply
+ endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages/${messageId}/replies`
+ } else {
+ // For chat messages, replies are just new messages
+ endpoint = `/chats/${chatChannelId}/messages`
+ }
+
+ const result = await this.makeTeamsRequest(endpoint, 'POST', body)
+
+ return this.formatResponse(
+ {
+ success: true,
+ reply: result,
+ message: 'Reply sent successfully',
+ context: teamId ? 'channel' : 'chat'
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error replying to message: ${error}`, params)
+ }
+ }
+}
+
+class SetReactionTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'set_reaction',
+ description: 'Set a reaction to a message',
+ schema: z.object({
+ chatChannelId: z.string().describe('ID of the chat or channel'),
+ teamId: z.string().optional().describe('ID of the team (required for channel messages)'),
+ messageId: z.string().describe('ID of the message to react to'),
+ reactionType: z
+ .enum(['like', 'heart', 'laugh', 'surprised', 'sad', 'angry'])
+ .optional()
+ .default('like')
+ .describe('Type of reaction to set')
+ }),
+ baseUrl: BASE_URL,
+ method: 'POST',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { chatChannelId, teamId, messageId, reactionType = 'like' } = params
+
+ if (!chatChannelId || !messageId) {
+ throw new Error('Chat/Channel ID and Message ID are required')
+ }
+
+ try {
+ let endpoint: string
+ if (teamId) {
+ // Channel message
+ endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages/${messageId}/setReaction`
+ } else {
+ // Chat message
+ endpoint = `/chats/${chatChannelId}/messages/${messageId}/setReaction`
+ }
+
+ const body = {
+ reactionType
+ }
+
+ await this.makeTeamsRequest(endpoint, 'POST', body)
+
+ return this.formatResponse(
+ {
+ success: true,
+ message: `Reaction "${reactionType}" set successfully`,
+ context: teamId ? 'channel' : 'chat'
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error setting reaction: ${error}`, params)
+ }
+ }
+}
+
+class UnsetReactionTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput = {
+ name: 'unset_reaction',
+ description: 'Remove a reaction from a message',
+ schema: z.object({
+ chatChannelId: z.string().describe('ID of the chat or channel'),
+ teamId: z.string().optional().describe('ID of the team (required for channel messages)'),
+ messageId: z.string().describe('ID of the message to remove reaction from'),
+ reactionType: z
+ .enum(['like', 'heart', 'laugh', 'surprised', 'sad', 'angry'])
+ .optional()
+ .default('like')
+ .describe('Type of reaction to remove')
+ }),
+ baseUrl: BASE_URL,
+ method: 'POST',
+ headers: {}
+ }
+
+ super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams })
+ }
+
+ protected async _call(arg: any): Promise {
+ const params = { ...arg, ...this.defaultParams }
+ const { chatChannelId, teamId, messageId, reactionType = 'like' } = params
+
+ if (!chatChannelId || !messageId) {
+ throw new Error('Chat/Channel ID and Message ID are required')
+ }
+
+ try {
+ let endpoint: string
+ if (teamId) {
+ // Channel message
+ endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages/${messageId}/unsetReaction`
+ } else {
+ // Chat message
+ endpoint = `/chats/${chatChannelId}/messages/${messageId}/unsetReaction`
+ }
+
+ const body = {
+ reactionType
+ }
+
+ await this.makeTeamsRequest(endpoint, 'POST', body)
+
+ return this.formatResponse(
+ {
+ success: true,
+ message: `Reaction "${reactionType}" removed successfully`,
+ context: teamId ? 'channel' : 'chat'
+ },
+ params
+ )
+ } catch (error) {
+ return this.formatResponse(`Error unsetting reaction: ${error}`, params)
+ }
+ }
+}
+
+class GetAllMessagesTool extends BaseTeamsTool {
+ constructor(args: { accessToken?: string; defaultParams?: any }) {
+ const toolInput: DynamicStructuredToolInput