From c5135849894a2ef410229569767790dd238620b8 Mon Sep 17 00:00:00 2001 From: Mateo Torres Date: Tue, 6 Jan 2026 10:55:08 -0300 Subject: [PATCH 1/6] improved the TS langchain guide --- app/en/guides/agent-frameworks/_meta.tsx | 2 +- .../use-arcade-with-langchain/page.mdx | 90 ++++++++++++++----- 2 files changed, 70 insertions(+), 22 deletions(-) diff --git a/app/en/guides/agent-frameworks/_meta.tsx b/app/en/guides/agent-frameworks/_meta.tsx index 0781b14df..294221288 100644 --- a/app/en/guides/agent-frameworks/_meta.tsx +++ b/app/en/guides/agent-frameworks/_meta.tsx @@ -11,7 +11,7 @@ export const meta: MetaRecord = { title: "Google ADK", }, langchain: { - title: "LangGraph", + title: "LangChain", }, mastra: { title: "Mastra", diff --git a/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx b/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx index d2be994b4..5a100164f 100644 --- a/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx +++ b/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx @@ -102,6 +102,28 @@ This is quite a number of imports, let's break them down: - `chalk`: This is a library to colorize the console output. - `readline`: This is a library to read input from the console. +### Configure the agent + +These variables are used in the rest of the code to customize the agent and manage the tools. Feel free to configure them to your liking. + +```ts filename="main.ts" +// configure your own values to customize your agent + +// The Arcade User ID identifies who is authorizing each service. +const arcadeUserID = "mateo@arcade.dev"; +// This determines which MCP server is providing the tools, you can customize this to make a Slack agent, or Notion agent, etc. +const toolkit = "gmail"; +// This determines the maximum number of tool definitions Arcade will return +const toolLimit = 30; +// This prompt defines the behavior of the agent. +const systemPrompt = + "You are a helpful assistant that can use Gmail tools. Your main task is to help the user with anything they may need."; +// This determines which LLM will be used inside the agent +const agentModel = "gpt-4o-mini"; +// This allows LangChain to retain the context of the session +const threadID = "1"; +``` + ### Write a helper function to execute Arcade tools ```ts filename="main.ts" @@ -169,13 +191,16 @@ In essence, this function is a wrapper around the `executeZodTool` function. Whe // Initialize the Arcade client const arcade = new Arcade(); -// Get the Arcade tools, you can customize the MCP Server (e.g. "github", "notion", "gmail", etc.) -const googleToolkit = await arcade.tools.list({ toolkit: "gmail", limit: 30 }); +// Get the Arcade tools +const toolDefinitions = await arcade.tools.list({ + toolkit: toolkit, + limit: toolLimit, +}); const arcadeTools = toZod({ - tools: googleToolkit.items, + tools: toolDefinitions.items, client: arcade, executeFactory: executeOrInterruptTool, - userId: "{arcade_user_id}", // Replace this with your application's user ID (e.g. email address, UUID, etc.) + userId: arcadeUserID, }); // Convert Arcade tools to LangGraph tools @@ -202,8 +227,11 @@ async function handleAuthInterrupt( const tool_name = value.tool_name; const authorization_response = value.authorization_response; console.log("⚙️: Authorization required for tool call", tool_name); - console.log("⚙️: Authorization URL", authorization_response.url); - console.log("⚙️: Waiting for authorization to complete..."); + console.log( + "⚙️: Please authorize in your browser", + authorization_response.url + ); + console.log("⚙️: Waiting for you to complete authorization..."); try { await arcade.auth.waitForCompletion(authorization_response.id); console.log("⚙️: Authorization granted. Resuming execution..."); @@ -225,9 +253,8 @@ It is important to note that this function captures the authorization flow outsi ```ts filename="main.ts" const agent = createAgent({ - systemPrompt: - "You are a helpful assistant that can use GMail tools. Your main task is to help the user with anything they may need.", - model: "gpt-4o-mini", + systemPrompt: systemPrompt, + model: agentModel, tools: tools, checkpointer: new MemorySaver(), }); @@ -273,7 +300,7 @@ Finally, write the main function that will call the agent and handle the user in ```ts filename="main.ts" async function main() { - const config = { configurable: { thread_id: "1" } }; + const config = { configurable: { thread_id: threadID } }; const rl = readline.createInterface({ input: process.stdin, output: process.stdout, @@ -325,7 +352,7 @@ async function main() { main().catch((err) => console.error(err)); ``` -Here the `config` object is used to configure the thread ID, which tells the agent to store the state of the conversation in a specific thread. Like any typical agent loop, you: +Here the `config` object is used to configure the `thread_id`, which tells the agent to store the state of the conversation in a specific thread. Like any typical agent loop, you: - Capture the user input - Stream the agent's response @@ -375,6 +402,22 @@ import { import chalk from "chalk"; import readline from "node:readline/promises"; +// configure your own values to customize your agent + +// The Arcade User ID identifies who is authorizing each service. +const arcadeUserID = "mateo@arcade.dev"; +// This determines which MCP server is providing the tools, you can customize this to make a Slack agent, or Notion agent, etc. +const toolkit = "gmail"; +// This determines the maximum number of tool definitions Arcade will return +const toolLimit = 30; +// This prompt defines the behavior of the agent. +const systemPrompt = + "You are a helpful assistant that can use Gmail tools. Your main task is to help the user with anything they may need."; +// This determines which LLM will be used inside the agent +const agentModel = "gpt-4o-mini"; +// This allows LangChain to retain the context of the session +const threadID = "1"; + function executeOrInterruptTool({ zodToolSchema, toolDefinition, @@ -433,13 +476,16 @@ function executeOrInterruptTool({ // Initialize the Arcade client const arcade = new Arcade(); -// Get the Arcade tools, you can customize the MCP Server (e.g. "github", "notion", "gmail", etc.) -const googleToolkit = await arcade.tools.list({ toolkit: "gmail", limit: 30 }); +// Get the Arcade tools +const toolDefinitions = await arcade.tools.list({ + toolkit: toolkit, + limit: toolLimit, +}); const arcadeTools = toZod({ - tools: googleToolkit.items, + tools: toolDefinitions.items, client: arcade, executeFactory: executeOrInterruptTool, - userId: "{arcade_user_id}", // Replace this with your application's user ID (e.g. email address, UUID, etc.) + userId: arcadeUserID, }); // Convert Arcade tools to LangGraph tools @@ -460,8 +506,11 @@ async function handleAuthInterrupt( const tool_name = value.tool_name; const authorization_response = value.authorization_response; console.log("⚙️: Authorization required for tool call", tool_name); - console.log("⚙️: Authorization URL", authorization_response.url); - console.log("⚙️: Waiting for authorization to complete..."); + console.log( + "⚙️: Please authorize in your browser", + authorization_response.url + ); + console.log("⚙️: Waiting for you to complete authorization..."); try { await arcade.auth.waitForCompletion(authorization_response.id); console.log("⚙️: Authorization granted. Resuming execution..."); @@ -475,9 +524,8 @@ async function handleAuthInterrupt( } const agent = createAgent({ - systemPrompt: - "You are a helpful assistant that can use GMail tools. Your main task is to help the user with anything they may need.", - model: "gpt-4o-mini", + systemPrompt: systemPrompt, + model: agentModel, tools: tools, checkpointer: new MemorySaver(), }); @@ -509,7 +557,7 @@ async function streamAgent( } async function main() { - const config = { configurable: { thread_id: "1" } }; + const config = { configurable: { thread_id: threadID } }; const rl = readline.createInterface({ input: process.stdin, output: process.stdout, From 6ad5571789a70737f72244ab2d0dc688844248c6 Mon Sep 17 00:00:00 2001 From: Mateo Torres Date: Thu, 8 Jan 2026 21:59:40 -0300 Subject: [PATCH 2/6] implemented Nabors' feedback from loom --- .../use-arcade-with-langchain/page.mdx | 196 ++++++++++++------ app/en/resources/glossary/page.mdx | 10 +- 2 files changed, 146 insertions(+), 60 deletions(-) diff --git a/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx b/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx index 5a100164f..26d9da898 100644 --- a/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx +++ b/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx @@ -33,12 +33,13 @@ Learn how to integrate Arcade tools using LangChain primitives -## A primer on agents and interrupts +## LangChain primitives you will use in this guide LangChain provides multiple abstractions for building AI agents, and it's very useful to internalize how some of these primitives work, so you can understand and extend the different agentic patterns LangChain supports. -- _Agents_: Most agentic frameworks, including LangChain, provide an abstraction for a ReAct agent. This is the most common agentic pattern, where an LLM runs in a loop, with the option to call tools to perform actions or retrieve information into its context. The initial input is a system prompt together with a user prompt. The agent may then iteratively call tools and update its context until it generates a response that does not involve a tool call. -- _Interrupts_: Interrupts in LangChain are a way to control the flow of the agentic loop when something needs to be done outside of the normal ReAct flow. For example, if a tool requires authorization, we can interrupt the agent and ask the user to authorize the tool before continuing. +- _Agents_: Most agentic frameworks, including LangChain, provide an abstraction for a ReAct agent. +- _Interrupts_: Interrupts in LangChain are a way to control the flow of the agentic loop when something needs to be done outside of the normal ReAct flow. For example, if a tool requires authorization, you can interrupt the agent and ask the user to authorize the tool before continuing. +- _Checkpointers_: Checkpointers are how LangChain implements persistence. A checkpointer stores the agent's state in a "checkpoint" that can be resumed later. Those checkpoints are saved to a _thread_, which can be accessed after the agent's execution, making it very simlpe for long-running agents and for handling interruptions and more sophisticated flows such as branching, time travel, and more. ## Integrate Arcade tools into a LangChain agent @@ -87,17 +88,17 @@ import readline from "node:readline/promises"; This is quite a number of imports, let's break them down: - Arcade imports: - - `Arcade`: This is the Arcade client, you'll use it to interact with the Arcade API. - - `type ToolExecuteFunctionFactoryInput`: This type encodes the input to execute Arcade tools. - - `executeZodTool`: You'll use this function to execute an Arcade tool. - - `isAuthorizationRequiredError`: You'll use this function to check if a tool requires authorization. - - `toZod`: You'll use this function to convert an Arcade tool definition into a Zod schema. + - `Arcade`: This is the Arcade client, used to interact with the Arcade API. + - `type ToolExecuteFunctionFactoryInput`: Encodes the input to execute Arcade tools. + - `isAuthorizationRequiredError`: Checks if a tool requires authorization. + - `toZod`: Converts an Arcade tool definition into a [Zod](https://zod.dev) schema. + - `executeZodTool`: Executes an Arcade tool. - LangChain imports: - - `createAgent`: This is the main function to create a LangChain agent. - - `tool`: You'll use this function to turn an Arcade tool definition into a LangChain tool. - - `Command`: You'll use this class to communicate the user's decisions to the agent's interrupts. - - `interrupt`: You'll use this function to interrupt the ReAct flow and ask the user for input. - - `MemorySaver`: This is a LangGraph construct that stores the agent's state, and is required for checkpointing and interrupts. + - `createAgent`: Creates a LangChain agent using the ReAct pattern. + - `tool`: Turns an Arcade tool definition into a LangChain tool. + - `interrupt`: Interrupts the ReAct flow and asks the user for input. + - `Command`: Communicates the user's decisions to the agent's interrupts. + - `MemorySaver`: Stores the agent's state, and is required for checkpointing and interrupts. - Other imports: - `chalk`: This is a library to colorize the console output. - `readline`: This is a library to read input from the console. @@ -110,9 +111,11 @@ These variables are used in the rest of the code to customize the agent and mana // configure your own values to customize your agent // The Arcade User ID identifies who is authorizing each service. -const arcadeUserID = "mateo@arcade.dev"; -// This determines which MCP server is providing the tools, you can customize this to make a Slack agent, or Notion agent, etc. -const toolkit = "gmail"; +const arcadeUserID = "{arcade_user_id}"; +// This determines which MCP server is providing the tools, you can customize this to make a Notion agent. All tools from the MCP servers defined in the array will be used. +const MCPServers = ["Slack"]; +// This determines individual tools. Useful to pick specific tools when you don't need all of them. +const individualTools = ["Gmail_ListEmails", "Gmail_SendEmail"]; // This determines the maximum number of tool definitions Arcade will return const toolLimit = 30; // This prompt defines the behavior of the agent. @@ -126,6 +129,8 @@ const threadID = "1"; ### Write a helper function to execute Arcade tools +This is a wrapper around the `executeZodTool` function. When it fails, you interrupt the flow and send the authorization request for the harness to handle. If the user authorizes the tool, the harness will reply with an `{authorized: true}` object, and the tool call will be retried without interrupting the flow. + ```ts filename="main.ts" function executeOrInterruptTool({ zodToolSchema, @@ -146,14 +151,14 @@ function executeOrInterruptTool({ })(input); return result; } catch (error) { - // If the tool requires authorization, we interrupt the flow and ask the user to authorize the tool + // If the tool requires authorization, interrupt the flow and ask the user to authorize the tool if (error instanceof Error && isAuthorizationRequiredError(error)) { const response = await client.tools.authorize({ tool_name: toolName, user_id: userId, }); - // We interrupt the flow here, and pass everything the handler needs to get the user's authorization + // Interrupt the flow here, and pass everything the handler needs to get the user's authorization const interrupt_response = interrupt({ authorization_required: true, authorization_response: response, @@ -161,7 +166,7 @@ function executeOrInterruptTool({ url: response.url ?? "", }); - // If the user authorized the tool, we retry the tool call without interrupting the flow + // If the user authorized the tool, retry the tool call without interrupting the flow if (interrupt_response.authorized) { const result = await executeZodTool({ zodToolSchema, @@ -171,7 +176,7 @@ function executeOrInterruptTool({ })(input); return result; } else { - // If the user didn't authorize the tool, we throw an error, which will be handled by LangChain + // If the user didn't authorize the tool, throw an error, which will be handled by LangChain throw new Error( `Authorization required for tool call ${toolName}, but user didn't authorize.` ); @@ -183,40 +188,111 @@ function executeOrInterruptTool({ } ``` -In essence, this function is a wrapper around the `executeZodTool` function. When it fails, you interrupt the flow and send the authorization request for the harness to handle (the app that is running the agent). If the user authorizes the tool, the harness will reply with an `{authorized: true}` object, and the tool call will be retried without interrupting the flow. - ### Retrieve Arcade tools and transform them into LangChain tools +Here you get the Arcade tools you want the agent to use, and transform them into LangChain tools. The first step is to initialize the Arcade client, and get the tools you want to use. Then, use the `toZod` function to convert the Arcade tools into a Zod schema, and pass it to the `executeOrInterruptTool` function to create a LangChain tool. + +The helper may seem very long at a first glance, but it is actually quite simple: + +- retrieve tools from all configured MCP servers (defined in the `MCPServers` variable) +- retrieve individual tools (defined in the `individualTools` variable) +- combine the tools from the MCP servers and the individual tools +- convert the Arcade tools to Zod tools +- convert the Zod tools to LangChain tools + +You then call the `getTools` function to get the tools you want the agent to use. + ```ts filename="main.ts" // Initialize the Arcade client const arcade = new Arcade(); -// Get the Arcade tools -const toolDefinitions = await arcade.tools.list({ - toolkit: toolkit, - limit: toolLimit, -}); -const arcadeTools = toZod({ - tools: toolDefinitions.items, - client: arcade, - executeFactory: executeOrInterruptTool, +export type GetToolsProps = { + arcade: Arcade; + mcpServers?: string[]; + individualTools?: string[]; + userId: string; + limit?: number; +}; + +export async function getTools({ + arcade, + mcpServers = [], + individualTools = [], + userId, + limit = 30, +}: GetToolsProps) { + if (mcpServers.length === 0 && individualTools.length === 0) { + throw new Error("At least one tool or toolkit must be provided"); + } + + // Get up to `limit` tools from each configured MCP server + const from_mcpServers = await Promise.all( + mcpServers.map(async (mcpServerName) => { + const definitions = await arcade.tools.list({ + toolkit: mcpServerName, + limit: limit, + }); + return definitions.items; + }) + ); + + // Get the individual tools + const from_individualTools = await Promise.all( + individualTools.map(async (toolName) => { + return await arcade.tools.get(toolName); + }) + ); + + // Combine the tools from the MCP servers and the individual tools + const all_tools = [...from_mcpServers.flat(), ...from_individualTools]; + const unique_tools = Array.from( + new Map(all_tools.map((tool) => [tool.qualified_name, tool])).values() + ); + + // Convert the Arcade tools to Zod tools + const arcadeTools = toZod({ + tools: unique_tools, + client: arcade, + executeFactory: executeOrInterruptTool, + userId: userId, + }); + + // Convert Arcade tools to LangGraph tools + const langchainTools = arcadeTools.map( + ({ name, description, execute, parameters }) => + (tool as Function)(execute, { + name, + description, + schema: parameters, + }) + ); + + return langchainTools; +} + +const tools = await getTools({ + arcade, + mcpServers: MCPServers, + individualTools: individualTools, userId: arcadeUserID, + limit: toolLimit, }); - -// Convert Arcade tools to LangGraph tools -const tools = arcadeTools.map(({ name, description, execute, parameters }) => - (tool as Function)(execute, { - name, - description, - schema: parameters, - }) -); ``` -Here you get the Arcade tools we want to use in our agent, and transform them into LangChain tools. The first step is to initialize the Arcade client, and get the tools we want to use. Then, use the `toZod` function to convert the Arcade tools into a Zod schema, and pass it to the `executeOrInterruptTool` function to create a LangChain tool. - ### Write the interrupt handler +In LangChain, each interrupt needs to be "resolved" for the flow to continue. In response to an interrupt, you need to return a decision object with the information needed to resolve the interrupt. In this case, the decision is whether the authorization was successful, in which case the tool call will be retried, or if the authorization failed, the flow will be interrupted with an error, and the agent will decide what to do next. + +This helper function receives an interrupt and returns a decision object. Decision objects can be of any serializable type (convertible to JSON). In this case, you return an object with a boolean flag indicating if the authorization was successful. + + + This function captures the authorization flow outside of the agent's context, + which is a good practice for security and context engineering. By handling + everything in the harness, you remove the risk of the LLM replacing the + authorization URL or leaking it, and you keep the context free from any + authorization-related traces, which reduces the risk of hallucinations. + + ```ts filename="main.ts" async function handleAuthInterrupt( interrupt: Interrupt @@ -245,12 +321,10 @@ async function handleAuthInterrupt( } ``` -This helper function receives an interrupt object and returns a decision object. In LangChain, decisions can be of any serializable type, so you can return any object you need to continue the flow. In this case, we return an object with a boolean flag indicating if the authorization was successful. - -It is important to note that this function captures the authorization flow outside of the agent's context, which is a good practice for security and context engineering. By handling everything in the harness, you remove the risk of the LLM replacing the authorization URL or leaking it, and you keep the context free from any authorization-related traces, which reduces the risk of hallucinations. - ### Create the agent +Here you create the agent using the `createAgent` function. You pass the system prompt, the model, the tools, and the checkpointer. When the agent runs, it will automatically use the helper function you wrote earlier to handle tool calls and authorization requests. + ```ts filename="main.ts" const agent = createAgent({ systemPrompt: systemPrompt, @@ -260,10 +334,10 @@ const agent = createAgent({ }); ``` -Here you create the agent using the `createAgent` function. You pass the system prompt, the model, the tools, and the checkpointer. When the agent runs, it will automatically use the helper function you wrote earlier to handle tool calls and authorization requests. - ### Write the invoke helper +This last helper function handles the streaming of the agent's response, and captures the interrupts. When an interrupt is detected, it is added to the `interrupts` array, and the flow is interrupted. If there are no interrupts, it will just stream the agent's to your console. + ```ts filename="main.ts" async function streamAgent( agent: any, @@ -292,12 +366,19 @@ async function streamAgent( } ``` -This last helper function handles the streaming of the agent's response, and captures the interrupts. When an interrupt is detected, it is added to the interrupts array, and the flow is interrupted. If there are no interrupts, it will just stream the agent's to the console. - ### Write the main function Finally, write the main function that will call the agent and handle the user input. +Here the `config` object is used to configure the `thread_id`, which tells the agent to store the state of the conversation into that specific thread. Like any typical agent loop, you: + +1. Capture the user input +1. Stream the agent's response +1. Handle any authorization interrupts +1. Resume the agent after authorization +1. Handle any errors +1. Exit the loop if the user wants to quit + ```ts filename="main.ts" async function main() { const config = { configurable: { thread_id: threadID } }; @@ -352,15 +433,6 @@ async function main() { main().catch((err) => console.error(err)); ``` -Here the `config` object is used to configure the `thread_id`, which tells the agent to store the state of the conversation in a specific thread. Like any typical agent loop, you: - -- Capture the user input -- Stream the agent's response -- Handle any authorization interrupts -- Resume the agent after authorization -- Handle any errors -- Exit the loop if the user wants to quit - ### Run the agent ```bash @@ -380,6 +452,12 @@ You should see the agent responding to your prompts like any model, as well as h - Context isolation: By handling the authorization flow outside of the agent's context, you remove the risk of the LLM replacing the authorization URL or leaking it, and you keep the context free from any authorization-related traces, which reduces the risk of hallucinations. - You can leverage the interrupts mechanism to handle human intervention in the agent's flow, useful for authorization flows, policy enforcement, or anything else that requires input from the user. +## Next Steps + +1. Try adding additional tools to the agent or modifying the tool catalog for a different use case. +2. Try extending the interrupt handler to handle more complex flows, such as human in the loop. +3. Try implementing a fully deterministic flow before the agent runs, use this deterministic phase to prepare the context for the agent, adding things like the current date, time, or any other information that is relevant to the task at hand. + ## Example code ```ts filename="main.ts" diff --git a/app/en/resources/glossary/page.mdx b/app/en/resources/glossary/page.mdx index 7921c187a..6f9a47951 100644 --- a/app/en/resources/glossary/page.mdx +++ b/app/en/resources/glossary/page.mdx @@ -15,7 +15,15 @@ graph TD ### Agent -An 'agent' is the application you are building. It can be a chatbot, a web application, a mobile app, or any other type of application that happens to use an LLM as part of its functionality. Agents interact with the world by calling tools. Helping you build, test, authenticate, and deploy tools is what Arcade is all about. +An 'agent' is the intelligent application you are building. It can be a chatbot, a web application, a mobile app, or any other type of application that happens to use an LLM as part of its functionality. Agents interact with the world by calling tools. Helping you build, test, authenticate, and deploy tools is what Arcade is all about. + +### Harness + +A 'harness' is the application that is running the agent. The agent determines the prompt, the tools, and the policies for every interaction. The harness is the software "glue" that determines _how_ all of these components work together. Are tools called over MCP? Is the OAuth URL opened in a browser? Is the agent able to access bash commands in the terminal? The harness is responsible for handling all of these details. + +### ReAct Agent + +This (Reason + Act) is the most common agentic pattern, where an LLM runs in a loop, with the option to call tools to perform actions or retrieve information into its context. The initial input is a system prompt together with a user prompt. The agent may then iteratively call tools and update its context until it generates a response that does not involve a tool call. ### Context From 8f77d89515b17f29d89ab5bcace9991e49738524 Mon Sep 17 00:00:00 2001 From: Mateo Torres Date: Thu, 8 Jan 2026 22:24:55 -0300 Subject: [PATCH 3/6] updated large code block at the bottom --- .../use-arcade-with-langchain/page.mdx | 92 ++++++++++++++----- 1 file changed, 71 insertions(+), 21 deletions(-) diff --git a/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx b/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx index 26d9da898..ed5a96e6d 100644 --- a/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx +++ b/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx @@ -483,9 +483,11 @@ import readline from "node:readline/promises"; // configure your own values to customize your agent // The Arcade User ID identifies who is authorizing each service. -const arcadeUserID = "mateo@arcade.dev"; -// This determines which MCP server is providing the tools, you can customize this to make a Slack agent, or Notion agent, etc. -const toolkit = "gmail"; +const arcadeUserID = "{arcade_user_id}"; +// This determines which MCP server is providing the tools, you can customize this to make a Notion agent. All tools from the MCP servers defined in the array will be used. +const MCPServers = ["Slack"]; +// This determines individual tools. Useful to pick specific tools when you don't need all of them. +const individualTools = ["Gmail_ListEmails", "Gmail_SendEmail"]; // This determines the maximum number of tool definitions Arcade will return const toolLimit = 30; // This prompt defines the behavior of the agent. @@ -554,27 +556,75 @@ function executeOrInterruptTool({ // Initialize the Arcade client const arcade = new Arcade(); -// Get the Arcade tools -const toolDefinitions = await arcade.tools.list({ - toolkit: toolkit, - limit: toolLimit, -}); -const arcadeTools = toZod({ - tools: toolDefinitions.items, - client: arcade, - executeFactory: executeOrInterruptTool, +export type GetToolsProps = { + arcade: Arcade; + mcpServers?: string[]; + individualTools?: string[]; + userId: string; + limit?: number; +}; + +export async function getTools({ + arcade, + mcpServers = [], + individualTools = [], + userId, + limit = 30, +}: GetToolsProps) { + if (mcpServers.length === 0 && individualTools.length === 0) { + throw new Error("At least one tool or toolkit must be provided"); + } + + // Todo(Mateo): Add pagination support + const from_mcpServers = await Promise.all( + mcpServers.map(async (mcpServerName) => { + const definitions = await arcade.tools.list({ + toolkit: mcpServerName, + limit: limit, + }); + return definitions.items; + }) + ); + + const from_individualTools = await Promise.all( + individualTools.map(async (toolName) => { + return await arcade.tools.get(toolName); + }) + ); + + const all_tools = [...from_mcpServers.flat(), ...from_individualTools]; + const unique_tools = Array.from( + new Map(all_tools.map((tool) => [tool.qualified_name, tool])).values() + ); + + const arcadeTools = toZod({ + tools: unique_tools, + client: arcade, + executeFactory: executeOrInterruptTool, + userId: userId, + }); + + // Convert Arcade tools to LangGraph tools + const langchainTools = arcadeTools.map( + ({ name, description, execute, parameters }) => + (tool as Function)(execute, { + name, + description, + schema: parameters, + }) + ); + + return langchainTools; +} + +const tools = await getTools({ + arcade, + mcpServers: MCPServers, + individualTools: individualTools, userId: arcadeUserID, + limit: toolLimit, }); -// Convert Arcade tools to LangGraph tools -const tools = arcadeTools.map(({ name, description, execute, parameters }) => - (tool as Function)(execute, { - name, - description, - schema: parameters, - }) -); - async function handleAuthInterrupt( interrupt: Interrupt ): Promise<{ authorized: boolean }> { From 9e9c114a4ce4c8fd9291a4dfa310971e8c910ca8 Mon Sep 17 00:00:00 2001 From: Mateo Torres Date: Fri, 9 Jan 2026 10:50:32 -0300 Subject: [PATCH 4/6] addressed issues detected by cursor bot --- .../use-arcade-with-langchain/page.mdx | 87 ++++++++++--------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx b/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx index ed5a96e6d..4de3077fc 100644 --- a/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx +++ b/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx @@ -396,28 +396,29 @@ async function main() { rl.pause(); try { - // Stream the agent response and collect any interrupts - const interrupts = await streamAgent( - agent, - { - messages: [{ role: "user", content: input }], - }, - config - ); - - // Handle authorization interrupts - const decisions: any[] = []; - for (const interrupt of interrupts) { - decisions.push(await handleAuthInterrupt(interrupt)); - } + let agentInput: any = { + messages: [{ role: "user", content: input }], + }; + + // Loop until no more interrupts + while (true) { + const interrupts = await streamAgent(agent, agentInput, config); + + if (interrupts.length === 0) { + break; // No more interrupts, we're done + } - // Resume agent after authorization - if (decisions.length > 0) { - await streamAgent( - agent, - new Command({ resume: { decisions } }), - config - ); + // Handle all interrupts + const decisions: any[] = []; + for (const interrupt of interrupts) { + decisions.push(await handleInterrupt(interrupt, rl)); + } + + // Resume with decisions, then loop to check for more interrupts + // Pass single decision directly, or array for multiple interrupts + agentInput = new Command({ + resume: decisions.length === 1 ? decisions[0] : decisions, + }); } } catch (error) { console.error(error); @@ -575,7 +576,6 @@ export async function getTools({ throw new Error("At least one tool or toolkit must be provided"); } - // Todo(Mateo): Add pagination support const from_mcpServers = await Promise.all( mcpServers.map(async (mcpServerName) => { const definitions = await arcade.tools.list({ @@ -700,28 +700,29 @@ async function main() { rl.pause(); try { - // Stream the agent response and collect any interrupts - const interrupts = await streamAgent( - agent, - { - messages: [{ role: "user", content: input }], - }, - config - ); - - // Handle authorization interrupts - const decisions: any[] = []; - for (const interrupt of interrupts) { - decisions.push(await handleAuthInterrupt(interrupt)); - } + let agentInput: any = { + messages: [{ role: "user", content: input }], + }; + + // Loop until no more interrupts + while (true) { + const interrupts = await streamAgent(agent, agentInput, config); + + if (interrupts.length === 0) { + break; // No more interrupts, we're done + } - // Resume agent after authorization - if (decisions.length > 0) { - await streamAgent( - agent, - new Command({ resume: { decisions } }), - config - ); + // Handle all interrupts + const decisions: any[] = []; + for (const interrupt of interrupts) { + decisions.push(await handleInterrupt(interrupt, rl)); + } + + // Resume with decisions, then loop to check for more interrupts + // Pass single decision directly, or array for multiple interrupts + agentInput = new Command({ + resume: decisions.length === 1 ? decisions[0] : decisions, + }); } } catch (error) { console.error(error); From 069e1eea69a61eb9e4a4a2679f6a1a122fcac43f Mon Sep 17 00:00:00 2001 From: Mateo Torres Date: Fri, 9 Jan 2026 17:46:44 -0300 Subject: [PATCH 5/6] implemented feedback notes from nabors' stream --- .../use-arcade-with-langchain/page.mdx | 26 +++++++++---------- app/en/resources/glossary/page.mdx | 4 +++ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx b/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx index 4de3077fc..04b8e8fda 100644 --- a/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx +++ b/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx @@ -37,9 +37,9 @@ Learn how to integrate Arcade tools using LangChain primitives LangChain provides multiple abstractions for building AI agents, and it's very useful to internalize how some of these primitives work, so you can understand and extend the different agentic patterns LangChain supports. -- _Agents_: Most agentic frameworks, including LangChain, provide an abstraction for a ReAct agent. -- _Interrupts_: Interrupts in LangChain are a way to control the flow of the agentic loop when something needs to be done outside of the normal ReAct flow. For example, if a tool requires authorization, you can interrupt the agent and ask the user to authorize the tool before continuing. -- _Checkpointers_: Checkpointers are how LangChain implements persistence. A checkpointer stores the agent's state in a "checkpoint" that can be resumed later. Those checkpoints are saved to a _thread_, which can be accessed after the agent's execution, making it very simlpe for long-running agents and for handling interruptions and more sophisticated flows such as branching, time travel, and more. +- [_Agents_](https://docs.langchain.com/oss/javascript/langchain/agents): Most agentic frameworks, including LangChain, provide an abstraction for a ReAct agent. +- [_Interrupts_](https://docs.langchain.com/oss/javascript/langgraph/interrupts): Interrupts in LangChain are a way to control the flow of the agentic loop when something needs to be done outside of the normal ReAct flow. For example, if a tool requires authorization, you can interrupt the agent and ask the user to authorize the tool before continuing. +- [_Checkpointers_](https://docs.langchain.com/oss/javascript/langgraph/persistence): Checkpointers are how LangChain implements persistence. A checkpointer stores the agent's state in a "checkpoint" that can be resumed later. Those checkpoints are saved to a _thread_, which can be accessed after the agent's execution, making it very simlpe for long-running agents and for handling interruptions and more sophisticated flows such as branching, time travel, and more. ## Integrate Arcade tools into a LangChain agent @@ -91,8 +91,8 @@ This is quite a number of imports, let's break them down: - `Arcade`: This is the Arcade client, used to interact with the Arcade API. - `type ToolExecuteFunctionFactoryInput`: Encodes the input to execute Arcade tools. - `isAuthorizationRequiredError`: Checks if a tool requires authorization. - - `toZod`: Converts an Arcade tool definition into a [Zod](https://zod.dev) schema. - - `executeZodTool`: Executes an Arcade tool. + - `toZod`: Converts an Arcade tool definition into a [Zod](https://zod.dev) schema (Zod provides type safety and validation at runtime). + - `executeZodTool`: Executes an Zod-converted tool. - LangChain imports: - `createAgent`: Creates a LangChain agent using the ReAct pattern. - `tool`: Turns an Arcade tool definition into a LangChain tool. @@ -115,8 +115,8 @@ const arcadeUserID = "{arcade_user_id}"; // This determines which MCP server is providing the tools, you can customize this to make a Notion agent. All tools from the MCP servers defined in the array will be used. const MCPServers = ["Slack"]; // This determines individual tools. Useful to pick specific tools when you don't need all of them. -const individualTools = ["Gmail_ListEmails", "Gmail_SendEmail"]; -// This determines the maximum number of tool definitions Arcade will return +const individualTools = ["Gmail_ListEmails", "Gmail_SendEmail", "Gmail_WhoAmI"]; +// This determines the maximum number of tool definitions Arcade will return per MCP server const toolLimit = 30; // This prompt defines the behavior of the agent. const systemPrompt = @@ -192,7 +192,7 @@ function executeOrInterruptTool({ Here you get the Arcade tools you want the agent to use, and transform them into LangChain tools. The first step is to initialize the Arcade client, and get the tools you want to use. Then, use the `toZod` function to convert the Arcade tools into a Zod schema, and pass it to the `executeOrInterruptTool` function to create a LangChain tool. -The helper may seem very long at a first glance, but it is actually quite simple: +This helper function is fairly long, here's a breakdown of what it does for clarity: - retrieve tools from all configured MCP servers (defined in the `MCPServers` variable) - retrieve individual tools (defined in the `individualTools` variable) @@ -411,7 +411,7 @@ async function main() { // Handle all interrupts const decisions: any[] = []; for (const interrupt of interrupts) { - decisions.push(await handleInterrupt(interrupt, rl)); + decisions.push(await handleAuthInterrupt(interrupt, rl)); } // Resume with decisions, then loop to check for more interrupts @@ -455,9 +455,9 @@ You should see the agent responding to your prompts like any model, as well as h ## Next Steps -1. Try adding additional tools to the agent or modifying the tool catalog for a different use case. -2. Try extending the interrupt handler to handle more complex flows, such as human in the loop. -3. Try implementing a fully deterministic flow before the agent runs, use this deterministic phase to prepare the context for the agent, adding things like the current date, time, or any other information that is relevant to the task at hand. +1. Try adding additional tools to the agent or modifying the tools in the catalog for a different use case by modifying the `MCPServers` and `individualTools` variables. +2. Try refactoring the `handleAuthInterrupt` function to handle more complex flows, such as human-in-the-loop. +3. Try implementing a fully deterministic flow before the agentic loop, use this deterministic phase to prepare the context for the agent, adding things like the current date, time, or any other information that is relevant to the task at hand. ## Example code @@ -715,7 +715,7 @@ async function main() { // Handle all interrupts const decisions: any[] = []; for (const interrupt of interrupts) { - decisions.push(await handleInterrupt(interrupt, rl)); + decisions.push(await handleAuthInterrupt(interrupt, rl)); } // Resume with decisions, then loop to check for more interrupts diff --git a/app/en/resources/glossary/page.mdx b/app/en/resources/glossary/page.mdx index 6f9a47951..231ed0d11 100644 --- a/app/en/resources/glossary/page.mdx +++ b/app/en/resources/glossary/page.mdx @@ -126,6 +126,10 @@ A 'tenant' is a collection of projects with unified billing details. It is the t A 'project' is a collection of agents and tools. It is smallest unit of organization and isolation in Arcade. You can have multiple projects, and each project can have multiple agents and tools. Accounts can be members of multiple projects, and each project will have different API keys. +### Arcade API + +The 'Arcade API' is a set of endpoints that allow you to interact with the Arcade platform. You can use the API to call tools, get tool definitions, manage user auth, and more. See the [Arcade API Reference](/references/api) for more details. + ### API Key An 'API key' is a secret key that is used to authenticate requests to the Arcade API. It is used to identify the project that the request is for. API keys are project-specific. From a84d6be7abb44e4a76f9d977d90734c0f39306dc Mon Sep 17 00:00:00 2001 From: Mateo Torres Date: Fri, 9 Jan 2026 18:10:04 -0300 Subject: [PATCH 6/6] addded who am I tool for better default UX --- .../langchain/use-arcade-with-langchain/page.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx b/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx index 04b8e8fda..16f73cb26 100644 --- a/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx +++ b/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx @@ -488,7 +488,7 @@ const arcadeUserID = "{arcade_user_id}"; // This determines which MCP server is providing the tools, you can customize this to make a Notion agent. All tools from the MCP servers defined in the array will be used. const MCPServers = ["Slack"]; // This determines individual tools. Useful to pick specific tools when you don't need all of them. -const individualTools = ["Gmail_ListEmails", "Gmail_SendEmail"]; +const individualTools = ["Gmail_ListEmails", "Gmail_SendEmail", "Gmail_WhoAmI"]; // This determines the maximum number of tool definitions Arcade will return const toolLimit = 30; // This prompt defines the behavior of the agent.