LangChain Integration
Matimo integrates with LangChain for both Python and TypeScript. Both SDKs expose a single convert_tools_to_langchain / convertToolsToLangChain function that wraps any Matimo tool as a LangChain StructuredTool.
Python LangChain Integration
Section titled “Python LangChain Integration”Installation
Section titled “Installation”pip install "matimo[langchain]" langchain-openai langchain# or with uvuv add "matimo[langchain]" langchain-openai langchainBasic Setup
Section titled “Basic Setup”import asyncioimport osfrom matimo import Matimo, convert_tools_to_langchain
async def main(): # 1. Load Matimo tools matimo = await Matimo.init(auto_discover=True)
# 2. Convert to LangChain StructuredTools (one call) lc_tools = convert_tools_to_langchain( matimo.list_tools(), matimo, credentials={'SLACK_BOT_TOKEN': os.environ['SLACK_BOT_TOKEN']}, )
print(f"{len(lc_tools)} LangChain tools ready")
asyncio.run(main())OpenAI 128-tool limit:
gpt-4o,gpt-4o-mini, and most OpenAI models reject requests with more than 128 tools bound. Filter to only the tools the agent needs for the current task:# Only Slack toolsslack_tools_def = [t for t in matimo.list_tools() if t.name.startswith('slack_')]lc_tools = convert_tools_to_langchain(slack_tools_def, matimo, credentials={...})# Or use search to find relevant toolsrelevant = matimo.search_tools('send message')lc_tools = convert_tools_to_langchain(relevant[:20], matimo, credentials={...})
Complete ReAct Agent Example
Section titled “Complete ReAct Agent Example”import asyncioimport osfrom matimo import ApprovalRequest, Matimo, convert_tools_to_langchainfrom langchain_openai import ChatOpenAIfrom langchain.agents import create_agent # LangChain 1.x
async def confirm(request: ApprovalRequest) -> bool: return input(f"Allow {request.tool_name}? [y/N] ").strip().lower() == "y"
async def run_slack_agent(): # Initialise Matimo; tools that need approval ask confirm() matimo = await Matimo.init(auto_discover=True, on_approval=confirm)
# Filter to Slack tools only (the package uses both slack- and slack_ names) slack_tools_def = [t for t in matimo.list_tools() if t.name.startswith('slack')] print(f"Loaded {len(slack_tools_def)} Slack tools")
# Convert to LangChain format lc_tools = convert_tools_to_langchain( slack_tools_def, matimo, credentials={'SLACK_BOT_TOKEN': os.environ['SLACK_BOT_TOKEN']}, )
# Build the agent agent = create_agent( ChatOpenAI(model='gpt-4o-mini', temperature=0), tools=lc_tools, system_prompt='You are a helpful Slack assistant.', )
# Test queries queries = [ 'List all channels', 'Get message history for #general', 'Send a test message to #general', ] for query in queries: print(f"\nUser: \"{query}\"") result = await agent.ainvoke({'messages': [('user', query)]}) print(f"Agent: {result['messages'][-1].content}")
asyncio.run(run_slack_agent())Manual ReAct Loop (No Agent Framework)
Section titled “Manual ReAct Loop (No Agent Framework)”For full control over the tool-call loop:
import asyncioimport jsonimport osfrom matimo import Matimo, convert_tools_to_langchainfrom langchain_openai import ChatOpenAIfrom langchain_core.messages import AIMessage, HumanMessage, ToolMessage
async def run_manual_agent(mission: str) -> None: matimo = await Matimo.init(auto_discover=True) lc_tools = convert_tools_to_langchain(matimo.list_tools(), matimo)
llm = ChatOpenAI(model='gpt-4o-mini', temperature=0).bind_tools(lc_tools) tool_map = {t.name: t for t in lc_tools}
messages = [HumanMessage(content=mission)]
for _ in range(10): # max 10 agent steps response: AIMessage = await llm.ainvoke(messages) messages.append(response)
if not response.tool_calls: print(f"Final answer: {response.content}") break
for call in response.tool_calls: tool = tool_map[call['name']] tool_result = await tool.ainvoke(call['args']) messages.append(ToolMessage( tool_call_id=call['id'], content=str(tool_result), ))
asyncio.run(run_manual_agent("List the Slack channels and send hello to #general"))API Reference: convert_tools_to_langchain
Section titled “API Reference: convert_tools_to_langchain”from matimo import convert_tools_to_langchain
def convert_tools_to_langchain( tools: list[ToolDefinition], matimo: Matimo, credentials: dict[str, str] | None = None,) -> list[StructuredTool]: ...Parameters:
| Parameter | Type | Description |
|---|---|---|
tools |
list[ToolDefinition] |
Matimo tool definitions to convert |
matimo |
Matimo |
Instance used to execute tools |
credentials |
dict[str, str] | None |
Per-call credential overrides |
Returns: List of langchain_core.tools.StructuredTool objects.
Raises: ImportError if langchain-core is not installed.
Secret Handling (Python)
Section titled “Secret Handling (Python)”Secret parameters are automatically excluded from the LangChain schema so the LLM never sees them. A parameter is treated as a secret when its name contains TOKEN, KEY, SECRET, or PASSWORD (case-insensitive).
# Tool parameters: SLACK_BOT_TOKEN, channel, text# convert_tools_to_langchain excludes SLACK_BOT_TOKEN from schema# and injects it at call time from credentials
lc_tools = convert_tools_to_langchain( [slack_tool], matimo, credentials={'SLACK_BOT_TOKEN': os.environ['SLACK_BOT_TOKEN']},)
# LLM only provides: channel, text# SLACK_BOT_TOKEN injected automaticallyApprovals and Unset Parameters (Python)
Section titled “Approvals and Unset Parameters (Python)”An agent’s calls go through the same governance as matimo.execute(). Calls to tools that need approval, requires_approval: true, HTTP DELETE or command tools (secure by default in 0.2.0), or a destructive keyword in a sql/command argument, ask the instance’s on_approval callback. Without one they raise MatimoError (“Destructive operation requires approval”), so give the instance a reviewer before handing the agent such tools:
async def ask_operator(request: ApprovalRequest) -> bool: return await my_ui.confirm(f"Allow {request.tool_name} with {request.params}?")
matimo = await Matimo.init(auto_discover=True, on_approval=ask_operator)lc_tools = convert_tools_to_langchain(matimo.list_tools(), matimo)LangChain fills every optional parameter the model leaves out with None. Since 0.2.0 those are dropped before the call, so a tool sees only the arguments the model actually chose and its own defaults apply.
Skills Integration (Python, Non-MCP)
Section titled “Skills Integration (Python, Non-MCP)”When running LangChain without an MCP server, use the skills helpers to implement progressive skill disclosure:
from matimo import Matimo, SkillRegistry, convert_tools_to_langchainfrom langchain_openai import ChatOpenAIfrom langchain_core.messages import SystemMessage, HumanMessage
async def run_skills_agent(): matimo = await Matimo.init(auto_discover=True) lc_tools = convert_tools_to_langchain(matimo.list_tools(), matimo)
# Level 1: lightweight metadata for system prompt skills_meta = [ {'name': s.name, 'description': s.description} for s in matimo._registry.get_all() # or use skill registry ] meta_block = '\n'.join( f"- **{s['name']}**: {s['description']}" for s in skills_meta ) system_prompt = ( f"You are a helpful agent.\n\n" f"Available skills:\n{meta_block}" )
llm = ChatOpenAI(model='gpt-4o-mini').bind_tools(lc_tools) user_message = 'How do I handle Slack rate limits?'
messages = [ SystemMessage(content=system_prompt), HumanMessage(content=user_message), ] response = await llm.ainvoke(messages) print(response.content)Error Handling (Python)
Section titled “Error Handling (Python)”As in TypeScript, a tool made by convert_tools_to_langchain never raises into the agent: a policy denial, a refused approval or an HTTP error comes back to the model as "Error: <message>", and the agent can explain it or try something else. Only the model call itself can raise out of ainvoke():
try: result = await agent.ainvoke({"messages": [("user", "Send an email")]})except Exception as e: # network, rate limit, invalid API key for the model print(f"Agent run failed: {e}")Use on_event to record tool failures (tool:execution_failed, tool:execution_denied, tool:approval_denied).
OAuth2 with LangChain (Python)
Section titled “OAuth2 with LangChain (Python)”import os
# Set tokens in environment before initialising Matimoos.environ['GMAIL_ACCESS_TOKEN'] = 'your-access-token'os.environ['GITHUB_TOKEN'] = 'your-github-token'
matimo = await Matimo.init(auto_discover=True)# Tokens are injected automatically by the HTTP executorWorking Examples
Section titled “Working Examples”See python/examples/langchain/ for complete examples:
| File | Description |
|---|---|
agents/langchain_agent.py |
Generic multi-provider ReAct agent |
agents/langchain_skills_policy_agent.py |
Agent with skills + policy |
slack/slack_langchain.py |
Slack-only agent |
github/github_langchain.py |
GitHub tools agent |
github/github_with_approval.py |
Agent with HITL approval flow |
gmail/gmail_langchain.py |
Gmail tools agent |
postgres/postgres_langchain.py |
PostgreSQL agent |
postgres/postgres_with_approval.py |
PostgreSQL with approval |
Run them:
cd python/examplescp .env.example .env # fill in API keysuv run python langchain/agents/langchain_agent.py# with a custom mission:uv run python langchain/agents/langchain_agent.py "List all open GitHub issues"Troubleshooting (Python)
Section titled “Troubleshooting (Python)”Tool not found:
tools = matimo.list_tools()print([t.name for t in tools]) # Check available tool namesMissing langchain-core:
pip install "matimo[langchain]"OAuth token missing:
export SLACK_BOT_TOKEN=xoxb-...TypeScript LangChain Integration
Section titled “TypeScript LangChain Integration”Overview
Section titled “Overview”Matimo provides a simple, unified API (convertToolsToLangChain) to convert tool definitions to LangChain-compatible format. This eliminates boilerplate and scales to many tools seamlessly.
Installation
Section titled “Installation”npm install matimo langchain @langchain/core# orpnpm add matimo langchain @langchain/coreThe Simplified Approach: convertToolsToLangChain
Section titled “The Simplified Approach: convertToolsToLangChain”Key Benefits
Section titled “Key Benefits”- One function, any tool: Works with all Matimo tools
- Automatic Zod schema generation: Parameters validated against tool definition
- Simple secret injection: Pass API keys explicitly
- LLM-friendly results: Formatted for agent consumption
- code: Lightweight & maintainable
Basic Integration
Section titled “Basic Integration”import { MatimoInstance, convertToolsToLangChain } from '@matimo/core';import { ChatOpenAI } from '@langchain/openai';import { createAgent } from 'langchain';
// 1. Load Matimo tools; tools that need approval ask the callbackconst matimo = await MatimoInstance.init({ autoDiscover: true, onApproval: async (request) => confirmWithUser(request),});
// 2. Convert the tools this agent needsconst langchainTools = await convertToolsToLangChain( matimo.listTools().filter((t) => t.name.startsWith('slack')), matimo, { SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN! });
// 3. Create the agentconst agent = createAgent({ model: new ChatOpenAI({ model: 'gpt-4o-mini' }), tools: langchainTools,});
// Run itconst result = await agent.invoke({ messages: [{ role: 'user', content: 'List all Slack channels' }],});
console.log('Agent response:', result.messages[result.messages.length - 1].content);OpenAI 128-tool limit:
gpt-4o,gpt-4o-mini, and most OpenAI models reject requests with more than 128 tools bound. Filter to only the tools the agent needs:const slackTools = matimo.listTools().filter(t => t.name.startsWith('slack'));const langchainTools = await convertToolsToLangChain(slackTools, matimo, secrets);
Complete LangChain Agent Example
Section titled “Complete LangChain Agent Example”import { MatimoInstance, convertToolsToLangChain } from '@matimo/core';import { ChatOpenAI } from '@langchain/openai';import { createAgent } from 'langchain';
async function runSlackAgent() { // Initialize Matimo; sending a message asks the callback if the tool requires approval const matimo = await MatimoInstance.init({ autoDiscover: true, onApproval: async (request) => confirmWithUser(request), });
// Get all Slack tools (the package uses both slack- and slack_ names) const slackTools = matimo.listTools().filter((t) => t.name.startsWith('slack'));
console.log(`Loaded ${slackTools.length} Slack tools`);
// Convert to LangChain format (one line!) const langchainTools = await convertToolsToLangChain(slackTools, matimo, { SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN!, });
// Create agent const agent = createAgent({ model: new ChatOpenAI({ model: 'gpt-4o-mini', temperature: 0 }), tools: langchainTools, });
// Test queries const queries = [ 'List all channels', 'Get message history for #general', 'Send a test message to #general', ];
for (const query of queries) { console.log(`\nUser: "${query}"`); const result = await agent.invoke({ messages: [{ role: 'user', content: query }] }); console.log(`Agent: ${result.messages[result.messages.length - 1].content}`); }}
runSlackAgent().catch(console.error);API Reference: convertToolsToLangChain
Section titled “API Reference: convertToolsToLangChain”export async function convertToolsToLangChain( tools: ToolDefinition[], matimo: MatimoInstance, secrets?: Record<string, string>): Promise<LangChainTool[]>;Parameters
Section titled “Parameters”tools: Array of Matimo tool definitions to convertmatimo: MatimoInstance for tool executionsecrets(optional), Object with secret values to inject- Keys: parameter names (e.g.,
SLACK_BOT_TOKEN,api_key) - Values: secret values from environment or storage
- Auto-detection: Parameters containing
TOKEN,KEY,SECRET, orPASSWORDare automatically treated as secrets
- Keys: parameter names (e.g.,
Returns
Section titled “Returns”Array of LangChain-compatible tools ready for agents.
Secret Handling
Section titled “Secret Handling”Explicit Secret Injection
Section titled “Explicit Secret Injection”const tools = await convertToolsToLangChain(matimo.listTools(), matimo, { SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN!, GMAIL_ACCESS_TOKEN: process.env.GMAIL_ACCESS_TOKEN!, api_key: process.env.MY_API_KEY!,});Auto-Detected Secret Parameters
Section titled “Auto-Detected Secret Parameters”How it works: The convertToolsToLangChain function automatically detects which parameters should be treated as secrets by scanning their names for common secret patterns. When a parameter is detected as a secret:
- It’s excluded from the LangChain schema (users don’t need to provide it)
- It’s injected automatically when present in the secrets map
- It’s never logged or exposed in error messages
Parameters are automatically detected as secrets if they match these patterns:
- Parameter name contains
TOKEN(e.g.,bot_token,access_TOKEN) - Parameter name contains
KEY(e.g.,api_key,encryption_KEY) - Parameter name contains
SECRET(e.g.,api_secret) - Parameter name contains
PASSWORD(e.g.,db_password) - Case-insensitive matching (e.g.,
ApiKeymatches theKEYpattern)
Example:
// Tool has parameters: slack_bot_token, channel// When passed to convertToolsToLangChain with { slack_bot_token: '...' }:// ✓ slack_bot_token is auto-detected as a secret and excluded from schema// ✓ Only channel appears in the LangChain schema// ✓ slack_bot_token is injected automatically on tool execution
const tools = await convertToolsToLangChain( [slackTool], // tool.parameters = { slack_bot_token, channel, ... } matimo, { slack_bot_token: process.env.SLACK_BOT_TOKEN! });
// User only provides: channel// slack_bot_token is injected automaticallyawait tools[0].invoke({ channel: '#general' });Working Examples
Section titled “Working Examples”See examples/tools/ for complete examples:
gmail-langchain.ts- Gmail tool integration with LangChaingmail-decorator.ts- Decorator pattern examplegmail-factory.ts- Factory pattern example
Run them:
cd examples/toolspnpm installpnpm gmail:langchain --email:your@email.comTool Parameter Mapping
Section titled “Tool Parameter Mapping”Matimo parameters map directly to LangChain function calls:
# Matimo tool definitionparameters: email: type: string required: true subject: type: string required: true body: type: string required: trueBecomes in LangChain:
{ "name": "gmail-send-email", "description": "Send an email", "parameters": { "type": "object", "properties": { "email": { "type": "string" }, "subject": { "type": "string" }, "body": { "type": "string" } }, "required": ["email", "subject", "body"] }}OAuth2 with LangChain
Section titled “OAuth2 with LangChain”Tools requiring OAuth2 authentication:
// Set OAuth tokens as environment variablesprocess.env.GMAIL_ACCESS_TOKEN = 'your-access-token';process.env.GITHUB_TOKEN = 'your-github-token';
// Matimo automatically injects tokens into toolsconst result = await matimoInstance.execute('gmail-send-email', { to: 'user@example.com', subject: 'Hello', body: 'Message', // Token is automatically included from environment});Error Handling
Section titled “Error Handling”Inside an agent, a tool failure does not throw out of agent.invoke(). The tools made by convertToolsToLangChain catch every MatimoError (policy denial, refused approval, HTTP error) and return it to the model as text, "Error: <message>", so the model can explain it or try something else. A tool that reports bad input returns { success: false, error, code }, which the model also sees.
What can still throw from invoke() is the model call itself (network, rate limits, an invalid API key):
try { const result = await agent.invoke({ messages: [{ role: 'user', content: 'Send an email' }], });} catch (error) { console.error('Agent run failed:', error);}To act on tool errors in your own code (metrics, alerts), use onEvent: every failed run emits tool:execution_failed, and refusals emit tool:execution_denied or tool:approval_denied.
Approvals
Section titled “Approvals”Agent calls go through the same governance as matimo.execute(). Calls to tools that need approval, requires_approval: true, HTTP DELETE or command tools (secure by default in 0.2.0), or a destructive keyword in a sql/command argument, ask the instance’s onApproval callback. If it declines, or there is none, the tool returns Error: ... to the model instead of running:
const matimo = await MatimoInstance.init({ autoDiscover: true, onApproval: async (request) => confirmWithOperator(request.toolName, request.params),});const tools = await convertToolsToLangChain(matimo.listTools(), matimo);See Approval System.
Skills Integration (Non-MCP)
Section titled “Skills Integration (Non-MCP)”When using Matimo with LangChain without an MCP server, skills are not surfaced as MCP Resources. Instead, use the two helper functions exported from matimo to implement the same progressive disclosure model programmatically:
| Helper | Level | When to call |
|---|---|---|
getSkillsMetadata(matimo) |
1, Discovery | Once at startup, inject into system prompt so the agent knows which skills exist |
buildRelevantSkillPrompt(matimo, query, options) |
2, Activation | Per request, semantic search (TF-IDF) loads full content only for top-K relevant skills |
Correct Pattern
Section titled “Correct Pattern”import { MatimoInstance, convertToolsToLangChain, getSkillsMetadata, buildRelevantSkillPrompt,} from 'matimo';import { ChatOpenAI } from '@langchain/openai';import { HumanMessage, SystemMessage } from '@langchain/core/messages';
// 1. Startup, Level 1 metadata block (token-safe, ~50 tokens/skill)const matimo = await MatimoInstance.init({ autoDiscover: true });const meta = getSkillsMetadata(matimo);// meta → [{ name: 'slack', description: 'Complete guide to all Slack tools…' }, …]
const metaBlock = meta .map((s) => `- **${s.name}**: ${s.description}`) .join('\n');
const systemPrompt = `You are a helpful agent.\n\nAvailable skills (use matimo_get_skill to load details):\n${metaBlock}`;
// 2. Per-request, Level 2 semantic search, loads only relevant contentconst userMessage = 'How do I handle Slack rate limits?';
const skillContext = await buildRelevantSkillPrompt(matimo, userMessage, { topK: 2, // Max skills to load (default: 3) minScore: 0.3, // Minimum relevance threshold (default: 0.3) header: 'Apply these skill guidelines:', // Optional custom header});// skillContext → markdown block with relevant skills embedded, or empty string
const messages = [ new SystemMessage(systemPrompt), ...(skillContext ? [new SystemMessage(skillContext)] : []), new HumanMessage(userMessage),];Why not load all skill content upfront?
Section titled “Why not load all skill content upfront?”The agentskills.io specification explicitly recommends against injecting all skill content into every system prompt:
- Skills vary per session, most requests need 1–2 skills, not all 15
- Large skill files can easily exceed 10,000 tokens each
buildRelevantSkillPromptuses TF-IDF cosine similarity to load only what’s relevant to the current query, cost proportional to relevance
For scenarios where a skill is always relevant (e.g. a Slack-only bot), load it directly with matimo.getSkillContent('slack') rather than using the semantic search helper.
getSkillsMetadata reference
Section titled “getSkillsMetadata reference”export function getSkillsMetadata( matimo: MatimoInstance): Array<{ name: string; description: string }>;Returns Level 1 metadata (name + description) for every registered skill. Does not load file content, always token-safe.
buildRelevantSkillPrompt reference
Section titled “buildRelevantSkillPrompt reference”export async function buildRelevantSkillPrompt( matimo: MatimoInstance, query: string, options?: { topK?: number; // Max skills to load (default: 3) minScore?: number; // Minimum TF-IDF cosine similarity (default: 0.3) header?: string; // Custom header line (optional) }): Promise<string>;Calls matimo.semanticSearchSkills(query, { limit: topK, minScore }) (TF-IDF) internally, then loads full content only for the top-matching skills. Returns a formatted markdown block ready to inject as a SystemMessage, or an empty string when no skills score above minScore.
Each skill block is formatted as:
## Skill: {name} (relevance: {score})_{description}_
{full SKILL.md content}Future Releases
Section titled “Future Releases”v0.2.0 will include:
- Official LangChain adapter package
- Automatic tool schema conversion
- LangChain Tool subclass implementation
- CrewAI integration examples
- Vercel AI SDK integration
Troubleshooting
Section titled “Troubleshooting”Tool Not Found Error
Section titled “Tool Not Found Error”Error: Tool not found: gmail-send-emailSolution: Verify tools are loaded correctly
const tools = matimoInstance.listTools();console.log( 'Available tools:', tools.map((t) => t.name));OAuth Token Missing
Section titled “OAuth Token Missing”Error: Missing OAuth token for provider: googleSolution: Set environment variable
export GMAIL_ACCESS_TOKEN=your_token_hereType Errors with LangChain Tools
Section titled “Type Errors with LangChain Tools”Ensure all Matimo tools are properly typed:
pnpm validate-tools # Validates all YAML definitionsSee Troubleshooting Guide for more help.