# Matimo: full documentation for LLMs > Matimo is an MIT-licensed, policy-governed tool-execution SDK for AI agents (TypeScript and Python). This file concatenates the core documentation pages. Summary and guidance for agents: https://matimo.dev/llms.txt . Every page below also exists as HTML at the Source URL. > Versions checked 2026-10-29: TypeScript 0.4.0 on npm (matimo, @matimo/core, @matimo/cli); Python matimo was 0.2.0 on PyPI when last checked. Where this text names a version, a newer release may exist; confirm on npm and PyPI. > Caveats that apply across all pages: the 15 meta-tools are in both SDKs (create, approve, reload and run were verified with pip install matimo 0.2.0 and with the TypeScript SDK); @matimo/composio is TypeScript only. The audit log is tamper evident, not tamper proof. --- # Why Matimo OSS: tool governance for AI agents Source: https://matimo.dev/docs/why-matimo/ # Why Matimo OSS Matimo OSS is an open-source (MIT) SDK that governs the tool calls an AI agent makes. It runs inside your process, in TypeScript and Python, and works with LangChain, CrewAI, Agno (Python) and any MCP client. ## The problem Giving an agent a tool is easy. The questions that follow are harder: who approved that delete, could the agent have approved its own tool, and can you trust the record of what ran? ## What Matimo OSS does - **Classifies every call by risk** (low, medium, high, critical) and checks it against deterministic security rules. - **Asks a human for risky calls.** Since 0.2.0, HTTP `DELETE` and shell-command tools ask before every call unless the tool says otherwise. With nobody to answer, the call is refused. See [Approval System](https://matimo.dev/docs/api-reference/approval-system.md). - **Stops an agent approving its own tool.** Roles such as `admin` are set by your application, never read from the agent's text. See [Where roles come from](https://matimo.dev/docs/api-reference/policy-and-lifecycle.md#where-roles-come-from). - **Writes an audit log you can verify.** Each event is chained to the previous one by SHA-256, with secrets redacted, and `verifyAuditLog()` finds an edited, removed or reordered line. - **Defines a tool once** in YAML and runs it in TypeScript, Python, LangChain, CrewAI, Agno and MCP, with the same rules in each. Shared fixtures in `conformance/` keep both SDKs aligned. - **Lets agents extend themselves safely:** a new tool starts as a draft, is validated, and must be approved by someone other than the agent that created it. See [Meta-Tools](https://matimo.dev/docs/api-reference/meta-tools.md). It ships 129 tools across 10 provider packages (Slack, Gmail, GitHub, Notion, HubSpot, Postgres, Twilio, Mailchimp, Microsoft Graph, Bruno), plus built-in tools and 15 meta-tools. ## Who it is for | If you are | You get | |---|---| | Building agents | One tool definition for every framework, and approval for destructive actions without writing it yourself | | Responsible for security or platform | A policy layer you can read, host and test, with a log you can verify | | An AI agent or coding assistant | Facts and honest limits in [llms.txt](https://github.com/tallclub/matimo/blob/main/docs/llms.txt) | ## How it fits with other tools - **A plain tool library** gives an agent functions to call. Matimo adds the checks before and the record after. - **A hosted tool gateway** runs governance on the vendor's servers. Matimo OSS runs in your process, so no vendor sits in the call path, and the log is a file you hold. Matimo can wrap a third-party catalog such as Composio's under the same policy layer, using your own key (`@matimo/composio`). ## What Matimo OSS does not do - It has no compliance certifications. - It governs tool calls. It does not replace your own authentication, and you supply the credentials. - The audit log is tamper-evident, not tamper-proof: someone who can rewrite the whole file can rebuild the chain, so keep it where the agent host can't write. - A call that needs approval fails closed if nobody can answer. Send the prompt to a real person. - Agno support is Python-only. The [release notes](https://matimo.dev/docs/releases.md) list the other differences between the SDKs. ## Try it ```bash npm install @matimo/core # TypeScript pip install matimo # Python ``` Then follow the [Quick Start](https://matimo.dev/docs/getting-started/quick-start.md), or read [what changed in 0.2.0](https://matimo.dev/docs/blog/2026-10-01-matimo-0-2-0-tool-governance.md). To see approval and audit working with no API key, run `pnpm policy:approval-modes` and `pnpm policy:audit` from `typescript/examples/tools`, or `approval_modes_demo.py` in `python/examples/native/policy/`. --- # Installation & Setup Source: https://matimo.dev/docs/getting-started/installation/ ## Choose Your SDK Matimo is available for both **TypeScript/Node.js** and **Python**. Pick the language that matches your project. | SDK | Package manager | Min version | |-----|----------------|-------------| | TypeScript | npm / pnpm | Node.js ≥ 22.19, pnpm ≥ 8.15 | | Python | pip / uv | Python ≥ 3.11 | --- ## Python SDK ### Requirements - **Python**: 3.11 or higher - **uv** (recommended) or pip ### Option 1: From PyPI (Recommended for Users) ```bash # Core SDK only pip install matimo # or with uv (recommended) uv add matimo # With LangChain integration pip install "matimo[langchain]" uv add "matimo[langchain]" # With Agno integration pip install "matimo[agno]" # With CrewAI integration pip install "matimo[crewai]" # With several extras (also available: mcp, dotenv, vault, aws, or all) pip install "matimo[langchain,crewai]" pip install "matimo[all]" ``` Provider packages are published separately: ```bash pip install matimo-slack matimo-github matimo-gmail uv add matimo-slack matimo-github matimo-gmail ``` ### Option 2: From Source (Recommended for Contributors) ```bash # Clone the repository git clone https://github.com/tallclub/matimo.git cd matimo/python # Install all workspace packages with extras and dev dependencies make install # uv sync --all-extras --dev # Run tests to verify installation make test ``` ### Verify Installation ```python # test_install.py import asyncio from matimo import Matimo async def main(): matimo = await Matimo.init(auto_discover=True) print(f"Matimo installed successfully") print(f"Loaded {len(matimo.list_tools())} tools") asyncio.run(main()) ``` Run it: ```bash python test_install.py # or with uv uv run python test_install.py ``` ### Python Next Steps - **New Users**: Go to [Quick Start](https://matimo.dev/docs/getting-started/quick-start.md#python-sdk) - **Building Tools**: Read [Tool Specification](https://matimo.dev/docs/tool-development/yaml-tools.md) (YAML format is identical) - **LangChain / CrewAI**: See [LangChain Integration](https://matimo.dev/docs/framework-integrations/langchain.md) - **Examples**: See [python/examples/](https://github.com/tallclub/matimo/blob/main/python/examples/) --- ## TypeScript SDK ### Requirements - **Node.js**: v22.19.0 or higher - **pnpm**: v8.15.0 or higher (preferred package manager) - **Git**: For cloning the repository ## Installation Options ### Option 1: From npm (Recommended for Users) ```bash # Install the SDK (core + CLI). Provider packages such as @matimo/slack install separately npm install matimo # or with pnpm (recommended) pnpm add matimo # OR install individual packages: pnpm add @matimo/core # Core SDK only pnpm add @matimo/slack # Slack tools pnpm add @matimo/gmail # Gmail tools pnpm add @matimo/cli # CLI tool management ``` **Note**: Check [npm](https://www.npmjs.com/package/@matimo/core) for the latest stable release. Future releases follow semantic versioning. ### Option 2: From Source (Recommended for Contributors) ```bash # Clone the repository git clone https://github.com/tallclub/matimo.git cd matimo/typescript # the pnpm workspace lives here, not at the repo root # Install dependencies pnpm install # Build TypeScript pnpm build # Run tests to verify installation pnpm test ``` ## Verify Installation ```typescript // test-install.ts - Both imports work: import { MatimoInstance } from 'matimo'; // From root package // OR import { MatimoInstance } from '@matimo/core'; // From core package directly const matimo = await MatimoInstance.init({ autoDiscover: true }); console.log(`Matimo installed successfully`); console.log(`Loaded ${matimo.listTools().length} tools`); ``` Run it: ```bash npx tsx test-install.ts ``` ## Next Steps - **New Users**: Go to [Quick Start](https://matimo.dev/docs/getting-started/quick-start.md) - **Want Examples**: See [examples/](https://github.com/tallclub/matimo/blob/main/typescript/examples/) - **Building Tools**: Read [Tool Specification](https://matimo.dev/docs/tool-development/yaml-tools.md) - **Using Decorators**: Check [Decorator Guide](https://matimo.dev/docs/tool-development/decorator-guide.md) ## Troubleshooting Installation ### Python: Version Error ```bash python --version # Should be 3.11+ ``` If lower, install Python 3.11+ from [python.org](https://python.org) or via pyenv: ```bash pyenv install 3.11 pyenv global 3.11 ``` ### Python: uv Installation ```bash curl -LsSf https://astral.sh/uv/install.sh | sh uv --version ``` ### Python: Import Error After Install ```bash # Verify the package is installed in the right environment pip show matimo # or uv pip show matimo ``` --- ## Troubleshooting Installation (TypeScript) ### Node.js Version Error ```bash node --version # Should be v22.19.0 or higher ``` If lower, install Node.js 22.19+ from [nodejs.org](https://nodejs.org/) ### pnpm Installation ```bash npm install -g pnpm@8.15.0 pnpm --version # Should be 8.15.0 or higher ``` ### Build Errors ```bash # Clear and rebuild (from typescript/) cd typescript pnpm clean pnpm install pnpm build ``` If issues persist, check [Troubleshooting Guide](https://matimo.dev/docs/troubleshooting/faq.md) ## System Requirements ### TypeScript SDK | Component | Requirement | | ---------- | -------------------------- | | Node.js | ≥ 22.19.0 | | pnpm | ≥ 8.15.0 | | TypeScript | ≥ 5.0 (included) | | Disk Space | ~500MB (with node_modules) | | Memory | ≥ 512MB for build | ### Python SDK | Component | Requirement | | ---------- | ---------------- | | Python | ≥ 3.11 | | uv | Latest (recommended) | | Disk Space | ~200MB (with venv) | | Memory | ≥ 256MB | ## IDE Setup ### VS Code (Recommended) ```bash # Extensions to install - ES7+ React/Redux/React-Native snippets - Prettier - Code formatter - ESLint - Thunder Client (for testing HTTP tools) ``` ### WebStorm / IntelliJ Works out of the box with TypeScript support. ## Environment Variables ### Security Settings **Embedded Code Execution** (TypeScript; disabled by default) Embedded code in tool YAML is **disabled by default** for security. To enable it: ```bash # Explicitly opt-in to embedded code execution # embeddedCodeDisabled = (MATIMO_ALLOW_EMBEDDED_CODE !== 'true') # If NOT set to 'true' → code is disabled export MATIMO_ALLOW_EMBEDDED_CODE=true ``` **Only enable if you fully trust all tool YAML sources. Never enable in production without careful review.** See the [Security Policy](https://github.com/tallclub/matimo/blob/main/SECURITY.md) and [Policy Engine](https://matimo.dev/docs/api-reference/policy-and-lifecycle.md) for more details. ## Quick Verification Checklist - [ ] Node.js v22.19+ installed - [ ] pnpm v8.15+ installed - [ ] Repository cloned - [ ] `pnpm install` completed - [ ] `pnpm build` successful - [ ] `pnpm test` all passing - [ ] Ready to start! --- # Quick Start: 5 Minutes Source: https://matimo.dev/docs/getting-started/quick-start/ Get Matimo up and running in 5 minutes. Available in **TypeScript** and **Python**. --- ## Python SDK ### Path D: Python Quick Start **Install:** ```bash pip install matimo # or with uv uv add matimo ``` **Use pre-built tools immediately:** ```bash pip install matimo matimo-slack matimo-github ``` ```python import asyncio from matimo import Matimo async def main(): # Load all installed provider packages automatically matimo = await Matimo.init(auto_discover=True) # Execute a Slack tool result = await matimo.execute('slack_send_channel_message', { 'channel': '#general', 'text': 'Hello from Matimo!', }) print('Message sent!', result) asyncio.run(main()) ``` > **Tool Naming** > You'll notice Matimo tools use both `kebab-case` (e.g., `slack-send-message`) and `snake_case` (e.g., `slack_send_channel_message`). > Both work identically, legacy tools use kebab-case, newer tools use snake_case. > **Recommended for new tools:** `snake_case` following `{provider}_{action}` (e.g., `notion_create_page`). **Build your own Python tool (5 min):** Most tools are a single YAML file that describes an HTTP call, no code. This one calls the public JSONPlaceholder test API. **1. Create `tools/get_user/definition.yaml`:** ```yaml name: get_user description: Look up a user by id on the JSONPlaceholder test API version: '1.0.0' parameters: id: type: number required: true description: User id (1-10) execution: type: http method: GET url: 'https://jsonplaceholder.typicode.com/users/{id}' output_schema: type: object properties: name: type: string email: type: string ``` **2. Create `main.py`:** ```python import asyncio from matimo import Matimo async def main(): matimo = await Matimo.init('./tools', log_level='warn') print(f"Loaded {len(matimo.list_tools())} tools") user = await matimo.execute('get_user', {'id': 1}) print('', user['name'], '-', user['email']) asyncio.run(main()) ``` **3. Run it:** ```bash python main.py # Loaded 1 tools # Leanne Graham - Sincere@april.biz ``` A Python HTTP tool returns the parsed response body. **Python next steps:** - [SDK Patterns (Python)](https://matimo.dev/docs/user-guide/sdk-patterns.md#python-sdk): factory, decorator, LangChain - [LangChain Integration](https://matimo.dev/docs/framework-integrations/langchain.md) - [Examples →](https://github.com/tallclub/matimo/blob/main/python/examples/) --- ## TypeScript SDK ## Choose Your Path Not sure where to start? Pick one: ### **Path A: Use Pre-Built Tools** (Fastest: 2 mins) You want to execute existing tools (Slack, Gmail, GitHub, etc.) without building your own. **Install:** ```bash npm install matimo @matimo/slack @matimo/gmail ``` **Use immediately:** ```typescript import { MatimoInstance } from 'matimo'; const matimo = await MatimoInstance.init({ autoDiscover: true }); // Execute a Slack tool const result = await matimo.execute('slack-send-message', { channel: '#general', text: 'Hello from Matimo!', }); console.log('Message sent!', result); ``` > **Tool Naming** > Matimo supports both `kebab-case` and `snake_case` tool names. Legacy tools (Gmail, GitHub, older Slack) use `kebab-case` (e.g., `slack-send-message`), while newer tools use `snake_case` (e.g., `bruno_run_request`, `matimo_create_tool`). > **Recommended:** Use `snake_case` for new tools following `{provider}_{action}`. Great for: Using existing integrations in your app **[See All Available Tools →](https://github.com/tallclub/matimo/blob/main/README.md#whats-included)** --- ### **Path B: Build Your Own Tool** (Educational: 5 mins) You want to understand how to create and execute custom tools. **[Continue below to create your first tool →](#1-installation-1-min)** Great for: Learning how Matimo works **[Build Your First Tool →](https://matimo.dev/docs/getting-started/your-first-tool.md)** --- ### **Path C: Integrate with LangChain** (Advanced: 10 mins) You want to use Matimo tools with an AI agent (LangChain, CrewAI, etc.). **[See LangChain Integration →](https://matimo.dev/docs/framework-integrations/langchain.md)** Great for: Building intelligent agents **[Examples →](https://github.com/tallclub/matimo/blob/main/typescript/examples/README.md)** --- ## 1. Installation (1 min) ```bash npm install matimo # or with pnpm pnpm add matimo ``` ## 2. Create Your First Script (3 min) Create a file `demo.ts`: ```typescript import { MatimoInstance } from 'matimo'; async function main() { // Initialize Matimo with your tools const matimo = await MatimoInstance.init('./tools'); // List available tools const tools = matimo.listTools(); console.log(`Loaded ${tools.length} tools`); // Execute a tool const result = (await matimo.execute('get_user', { id: 1 })) as { data: { name: string; email: string }; }; console.log('', result.data.name, '-', result.data.email); } main().catch(console.error); ``` ## 3. Create Your First Tool (1 min) Create `tools/get_user/definition.yaml`, an HTTP tool, Matimo's default kind, which needs no code: ```yaml name: get_user description: Look up a user by id on the JSONPlaceholder test API version: '1.0.0' parameters: id: type: number required: true description: User id (1-10) execution: type: http method: GET url: 'https://jsonplaceholder.typicode.com/users/{id}' output_schema: type: object properties: name: type: string email: type: string ``` ## 4. Run It (< 1 min) ```bash # Compile TypeScript npx tsc demo.ts # Run the script node demo.js # Output: # Loaded 1 tools # Leanne Graham - Sincere@april.biz ``` A TypeScript HTTP tool returns `{ success, data, statusCode, headers }`, with the response body in `data`. --- ## What Just Happened? 1. **MatimoInstance.init('./tools')**: Loaded every `definition.yaml` under `./tools` 2. **matimo.listTools()**: Listed discovered tools 3. **matimo.execute('get_user', { id: 1 })**: The policy engine classified the call (a GET is low risk, so it runs without asking), Matimo filled `{id}` into the URL, made the request and returned the result ### The `MATIMO_APPROVAL_SECRET` warning On a first run both SDKs log `No MATIMO_APPROVAL_SECRET set`. It is a warning, not an error. Matimo signs approvals with that secret, and without one it makes a temporary secret for the process, so approvals do not survive a restart. For local experiments you can ignore it. Before you rely on approvals, set a long random value (`export MATIMO_APPROVAL_SECRET=...`). It is required when `NODE_ENV` or `MATIMO_ENV` is `production`. ### Tools that ask first In 0.2.0 some calls wait for a person: tools whose YAML says `requires_approval: true`, HTTP `DELETE` tools, and `type: command` tools (unless they say `requires_approval: false`). Give the instance a reviewer, or those calls are refused: ```typescript const matimo = await MatimoInstance.init({ toolPaths: ['./tools'], onApproval: async (request) => confirmWithUser(request.toolName, request.params), }); ``` ```python async def on_approval(request) -> bool: return await confirm_with_user(request.tool_name, request.params) matimo = await Matimo.init("./tools", on_approval=on_approval) ``` See [Approval System](https://matimo.dev/docs/api-reference/approval-system.md). --- ## Next Steps ### Choose Your Pattern - **Factory Pattern** (you just used this), Best for simple scripts and backends - **Decorator Pattern**: Best for class-based code - **LangChain**: Best for AI agents with automatic tool selection See [SDK Usage Patterns](https://matimo.dev/docs/user-guide/sdk-patterns.md) for details. ### Add More Tools Create more YAML files in `tools/`: ``` tools/ ├── get_user/ │ └── definition.yaml # Done ├── my-api/ │ └── definition.yaml # Create more └── slack/ └── definition.yaml ``` Each tool is just a YAML file, no code needed! ### Use Provider Tools Install pre-built tools from npm: ```bash pnpm add @matimo/slack @matimo/gmail ``` Then load them alongside your custom tools: ```typescript const matimo = await MatimoInstance.init({ toolPaths: [ './tools', // Your custom tools './node_modules/@matimo/slack/tools', // Pre-built tools './node_modules/@matimo/gmail/tools', ], }); ``` In Python, install the provider packages from PyPI and let auto-discovery find them, with your own folder alongside: ```bash pip install matimo-slack matimo-gmail ``` ```python matimo = await Matimo.init("./tools", auto_discover=True) ``` --- ## Further Reading - **[API Reference](https://matimo.dev/docs/api-reference/sdk.md)**: Full SDK methods and types - **[Tool Specification](https://matimo.dev/docs/tool-development/tool-specification.md)**: Write production tools - **[SDK Usage Patterns](https://matimo.dev/docs/user-guide/sdk-patterns.md)**: Factory, Decorator, LangChain patterns - **[Architecture Overview](https://matimo.dev/docs/architecture/overview.md)**: How Matimo works internally - **[Framework Integrations](https://matimo.dev/docs/framework-integrations/langchain.md)**: LangChain integration examples --- ## Common Tasks Each task is shown in TypeScript, then Python. ### List All Loaded Tools ```typescript const tools = matimo.listTools(); tools.forEach((tool) => { console.log(`${tool.name} - ${tool.description}`); }); ``` ```python for tool in matimo.list_tools(): print(f"{tool.name} - {tool.description}") ``` ### Get Tool by Name ```typescript const tool = matimo.getTool('get_user'); if (tool) { console.log('Parameters:', tool.parameters); } ``` ```python tool = matimo.get_tool("get_user") if tool: print("Parameters:", tool.parameters) ``` ### Search Tools ```typescript const results = matimo.searchTools('user'); console.log( 'Found:', results.map((t) => t.name) ); ``` ```python results = matimo.search_tools("user") print("Found:", [t.name for t in results]) ``` ### Execute with Error Handling ```typescript try { const result = await matimo.execute('get_user', { id: 1 }); console.log('Success:', result); } catch (error) { if (error.code === 'TOOL_NOT_FOUND') { console.error('Tool not found:', error.message); } else if (error.code === 'INVALID_PARAMETER') { console.error('Invalid parameters:', error.details); } else if (error.code === 'POLICY_DENIED') { console.error('Refused by policy or by a reviewer:', error.message); } else if (error.code === 'EXECUTION_FAILED') { console.error('Execution failed:', error.details); } } ``` ```python from matimo import MatimoError try: result = await matimo.execute("get_user", {"id": 1}) print("Success:", result) except MatimoError as error: if error.code == "TOOL_NOT_FOUND": print("Tool not found:", error) elif error.code == "INVALID_PARAMETER": print("Invalid parameters:", error.details) elif error.code == "POLICY_DENIED": print("Refused by policy or by a reviewer:", error) elif error.code == "EXECUTION_FAILED": print("Execution failed:", error.details) ``` --- ## Example: Using Slack After installing `@matimo/slack`: ```typescript import { MatimoInstance } from 'matimo'; const matimo = await MatimoInstance.init({ toolPaths: ['./tools', './node_modules/@matimo/slack/tools'], }); // Execute a Slack tool const result = await matimo.execute('slack-send-message', { channel: '#general', text: 'Hello from Matimo!', }); console.log(result); ``` The same call in Python, after `pip install matimo-slack`: ```python matimo = await Matimo.init("./tools", auto_discover=True) result = await matimo.execute("slack-send-message", { "channel": "#general", "text": "Hello from Matimo!", }) print(result) ``` --- ## Troubleshooting **Tools not loading?** ```bash # Check that YAML files exist ls tools/*/definition.yaml # Validate YAML syntax pnpm validate-tools # TypeScript: from typescript/ in a clone of the repo make validate-tools # Python: from python/ in a clone of the repo ``` **Execution failing?** ```typescript // Enable detailed error messages try { const result = await matimo.execute('tool-name', params); } catch (error) { console.error('Full error:', JSON.stringify(error, null, 2)); } ``` ```python # Python: print the full error, including its details try: result = await matimo.execute("tool-name", params) except MatimoError as error: print(error.code, error, error.details) ``` **Type errors?** ```bash # Check TypeScript compilation npx tsc --noEmit ``` --- ## Support - [Full Documentation](/docs/) - [GitHub Discussions](https://github.com/tallclub/matimo/discussions) - [Report Issues](https://github.com/tallclub/matimo/issues) - [Contributing](https://github.com/tallclub/matimo/blob/main/CONTRIBUTING.md) --- # Your First Tool: Step by Step Source: https://matimo.dev/docs/getting-started/your-first-tool/ Execute your first Matimo tool in 5 minutes. ## Prerequisites - Matimo installed, see [Installation](https://matimo.dev/docs/getting-started/installation.md): - **TypeScript**: `npm install matimo` and Node.js v22.19+ - **Python**: `pip install matimo` and Python 3.11+ Every step shows both SDKs. Pick one and follow it through. This guide uses `calculator`, one of the core tools that ship with Matimo. ## Step 1: Initialize Matimo ### TypeScript Create `first-tool.ts`: ```typescript import { MatimoInstance } from 'matimo'; async function main() { // Load Matimo's core tools, plus any @matimo/* provider packages you installed const m = await MatimoInstance.init({ autoDiscover: true }); console.log(`Loaded ${m.listTools().length} tools`); } main().catch(console.error); ``` Run it: ```bash npx tsx first-tool.ts ``` **Output:** ``` Loaded 24 tools ``` (24 core tools; each provider package you install adds its own.) ### Python Create `first_tool.py`: ```python import asyncio from matimo import Matimo async def main(): # Load Matimo's core tools, plus any matimo-* provider packages you installed m = await Matimo.init(auto_discover=True) print(f"Loaded {len(m.list_tools())} tools") asyncio.run(main()) ``` Run it with `python first_tool.py`. You get the 24 core tools plus the tools of every `matimo-*` provider package installed in that environment (a plain `pip install matimo` prints `Loaded 24 tools`). On first run Matimo also logs a `MATIMO_APPROVAL_SECRET` warning, explained in [Quick Start](https://matimo.dev/docs/getting-started/quick-start.md#the-matimo_approval_secret-warning). --- ## Step 2: List Available Tools **TypeScript**: update `first-tool.ts`: ```typescript import { MatimoInstance } from 'matimo'; async function main() { const m = await MatimoInstance.init({ autoDiscover: true }); // List all tools const tools = m.listTools(); console.log('\nAvailable Tools:\n'); tools.forEach((tool) => { console.log(` • ${tool.name}`); console.log(` ${tool.description}\n`); }); } main().catch(console.error); ``` **Python**: update `first_tool.py`: ```python import asyncio from matimo import Matimo async def main(): m = await Matimo.init(auto_discover=True) print("\nAvailable Tools:\n") for tool in m.list_tools(): print(f" - {tool.name}") print(f" {tool.description}\n") asyncio.run(main()) ``` **Output:** ``` Available Tools: • calculator Perform arithmetic operations. Two mutually exclusive modes are supported: ... • convert_to_file Convert structured content between JSON, CSV, Markdown, and plain text ... ... (more tools) ``` --- ## Step 3: Get Tool Details ```typescript const tool = m.getTool('calculator'); console.log(`Tool: ${tool.name}`); console.log(`Description: ${tool.description}`); console.log(`Parameters:`, tool.parameters); ``` ```python tool = m.get_tool("calculator") print(f"Tool: {tool.name}") print(f"Description: {tool.description}") print("Parameters:", list(tool.parameters)) # operation, a, b, expression, precision ``` **Output:** ``` Tool: calculator Description: Perform arithmetic operations. Two mutually exclusive modes are supported: ... Parameters: { operation: { type: 'string', enum: [...], ... }, a: { type: 'number', ... }, b: { type: 'number', ... }, expression: { type: 'string', ... }, precision: { type: 'number', ... } } ``` --- ## Step 4: Execute a Tool ```typescript const result = await m.execute('calculator', { operation: 'add', a: 5, b: 3, }); console.log('Result:', result); // Output: Result: { result: 8, operation: 'add', original_operation: 'add', operands: { a: 5, b: 3 } } ``` ```python result = await m.execute("calculator", {"operation": "add", "a": 5, "b": 3}) print("Result:", result) # Result: {'result': 8.0, 'operation': 'add', 'original_operation': 'add', 'operands': {'a': 5.0, 'b': 3.0}} ``` ### Execute Different Operations ```typescript // Add const add = await m.execute('calculator', { operation: 'add', a: 10, b: 5 }); console.log('10 + 5 =', add.result); // 15 // Subtract const sub = await m.execute('calculator', { operation: 'subtract', a: 10, b: 5 }); console.log('10 - 5 =', sub.result); // 5 // Multiply const mul = await m.execute('calculator', { operation: 'multiply', a: 10, b: 5 }); console.log('10 * 5 =', mul.result); // 50 // Divide const div = await m.execute('calculator', { operation: 'divide', a: 10, b: 5 }); console.log('10 / 5 =', div.result); // 2 ``` ```python for op in ("add", "subtract", "multiply", "divide"): r = await m.execute("calculator", {"operation": op, "a": 10, "b": 5}) print(f"10 {op} 5 =", r["result"]) # 15.0, 5.0, 50.0, 2.0 ``` --- ## Step 5: Handle Errors A tool can report a failure in its result, or Matimo can throw: ```typescript // The calculator reports division by zero in its result const div0 = await m.execute('calculator', { operation: 'divide', a: 10, b: 0 }); // { success: false, error: 'Division by zero', code: 'EXECUTION_FAILED', details: { a: 10, b: 0 } } // Matimo throws a MatimoError for unknown tools, bad parameters, policy refusals, ... try { await m.execute('no_such_tool', {}); } catch (error) { if (error.code === 'TOOL_NOT_FOUND') { console.error('Tool not found:', error.message); } else if (error.code === 'INVALID_PARAMETER') { console.error('Invalid parameters:', error.details); } else { console.error('Error:', error.code, error.message); } } ``` ```python from matimo import MatimoError # Python raises on a tool failure, including division by zero try: await m.execute("calculator", {"operation": "divide", "a": 10, "b": 0}) except MatimoError as error: print(error.code) # EXECUTION_FAILED # Unknown tools, bad parameters and policy refusals raise too try: await m.execute("no_such_tool", {}) except MatimoError as error: if error.code == "TOOL_NOT_FOUND": print("Tool not found:", error) elif error.code == "INVALID_PARAMETER": print("Invalid parameters:", error.details) else: print("Error:", error.code, error) ``` The two SDKs differ here: TypeScript returns `{ success: false, ... }` when the calculator hits a division by zero, while Python raises `MatimoError`. See [Error Codes](https://matimo.dev/docs/api-reference/errors.md) for all possible errors. --- ## Next Steps - **Learn SDK Patterns**: [Factory vs Decorator](https://matimo.dev/docs/user-guide/sdk-patterns.md) - **Find More Tools**: [Tool Discovery](https://matimo.dev/docs/user-guide/tool-discovery.md) - **Use with LangChain**: [LangChain Integration](https://matimo.dev/docs/framework-integrations/langchain.md) - **Build Your Own Tools**: [YAML Tool Specification](https://matimo.dev/docs/tool-development/yaml-tools.md) --- ## Complete Example ### TypeScript ```typescript import { MatimoInstance } from 'matimo'; async function main() { // 1. Initialize const m = await MatimoInstance.init({ autoDiscover: true }); console.log(`Loaded ${m.listTools().length} tools\n`); // 2. List tools const tools = m.listTools(); console.log('Available tools:'); tools.forEach((t) => console.log(` - ${t.name}`)); // 3. Get tool const calc = m.getTool('calculator'); console.log(`\nTool: ${calc.name}`); // 4. Execute console.log('\nExecuting calculator...'); const result = await m.execute('calculator', { operation: 'add', a: 5, b: 3, }); console.log('Result:', result.result); } main().catch(console.error); ``` **Output:** ``` Loaded 24 tools Available tools: - calculator - convert_to_file - edit ... (more tools) Tool: calculator Executing calculator... Result: 8 ``` ### Python ```python import asyncio from matimo import Matimo async def main(): # 1. Initialize m = await Matimo.init(auto_discover=True) print(f"Loaded {len(m.list_tools())} tools\n") # 2. List tools print("Available tools:") for t in m.list_tools(): print(f" - {t.name}") # 3. Get tool calc = m.get_tool("calculator") print(f"\nTool: {calc.name}") # 4. Execute print("\nExecuting calculator...") result = await m.execute("calculator", {"operation": "add", "a": 5, "b": 3}) print("Result:", result["result"]) # 8.0 asyncio.run(main()) ``` --- # Require approval for risky agent calls Source: https://matimo.dev/docs/use-cases/approve-risky-calls/ **The job:** your agent can call real tools, and you want a person to say yes before the dangerous ones run. Since Matimo 0.2.0 this is on by default. HTTP `DELETE` tools and command (shell) tools ask on every call. A tool can also opt in with `requires_approval: true` in its YAML. ## 1. Give it someone to ask A call that needs approval and has nobody to ask fails closed. You supply the person (or the UI) as a callback. ```typescript import { MatimoInstance, JsonlFileSink, verifyAuditLog, type ApprovalRequest } from '@matimo/core'; const matimo = await MatimoInstance.init({ toolPaths: ['./tools'], onApproval: async (request: ApprovalRequest) => { // request.toolName, request.description, request.params return await askReviewer(request); // true = run it, false = refuse }, auditSink: new JsonlFileSink('./audit.jsonl'), }); ``` ```python from matimo import ApprovalRequest, Matimo, JsonlFileSink, verify_audit_log async def on_approval(request: ApprovalRequest) -> bool: # request.tool_name, request.description, request.params return await ask_reviewer(request) matimo = await Matimo.init( "./tools", on_approval=on_approval, audit_sink=JsonlFileSink("./audit.jsonl"), ) ``` If the callback returns `false`, the call is refused and the agent gets an error. ## 2. Check the record Every call ends in an audit event. With `JsonlFileSink` each line is linked to the one before it, and verification reports the first line that was edited. ```typescript verifyAuditLog('./audit.jsonl'); // { valid: true, entries: 3 } ``` ```python verify_audit_log("./audit.jsonl") # AuditLogVerification(valid=True, entries=3) ``` The [home page demo](https://matimo.dev/#try-it) replays a real run of this flow. ## Limits - The log is **tamper evident, not tamper proof.** Verification shows an edit or a deleted line. It does not prevent one. - You write the reviewer. Matimo asks; it does not decide who your reviewers are. - With no `onApproval`, a call that needs approval fails. That is the intended behavior. - `MATIMO_AUTO_APPROVE=true` approves everything unseen. Use it only in throwaway test environments. ## Read next - [Approval System](https://matimo.dev/docs/api-reference/approval-system.md): which calls need a human, patterns and testing - [Policy Engine and Tool Lifecycle](https://matimo.dev/docs/api-reference/policy-and-lifecycle.md): risk classes, security rules and the audit log --- # Serve governed tools to Claude Desktop and Cursor Source: https://matimo.dev/docs/use-cases/serve-tools-over-mcp/ **The job:** give an MCP client (Claude Desktop, Cursor, Windsurf) a set of tools you choose, with the risky ones still behind a person. ## 1. Start the server ```bash npm install matimo npx matimo mcp setup # writes the Claude Desktop config npx matimo mcp --tool-paths ./tools # serve your tool directory over stdio ``` `npm install matimo` installs the core SDK and the CLI. Provider packages install separately, for example `npm install @matimo/slack`. For remote agents use `--transport http`, with a bearer token and TLS. See [MCP Server](https://matimo.dev/docs/mcp-server.md) for client configuration for each tool. ## 2. Decide what is visible ```bash # only these tools npx matimo mcp --tools slack_send_channel_message,slack-list-channels # everything except this one npx matimo mcp --exclude postgres-execute-sql ``` ## 3. Keep risky calls behind a person When a call needs approval, the MCP server asks the person using the client through an MCP elicitation request. The call runs only if they accept. A client without elicitation gets an error saying there is no one to ask. The model is never given a way to approve its own call. Details are in [Approval over MCP](https://matimo.dev/docs/mcp-server.md#approval-over-mcp). ## Limits - Matimo does not authenticate your users. You supply the credentials for each tool, through environment variables, `.env`, Vault or AWS Secrets Manager. - Whether approval prompts appear depends on the client supporting elicitation. - The `matimo mcp` command line does not set `trustClientApproval` or `context`. Those are programmatic options. ## Read next - [MCP Server](https://matimo.dev/docs/mcp-server.md): transports, client config, secrets, filtering and approval - [Authentication](https://matimo.dev/docs/user-guide/authentication.md): how credentials reach a tool call --- # Let an agent write tools safely Source: https://matimo.dev/docs/use-cases/agent-writes-tools/ **The job:** let an agent create a new capability at runtime, without letting it run unreviewed code. The 15 [meta-tools](https://matimo.dev/docs/api-reference/meta-tools.md) are in both SDKs. The flow below was run with a plain `pip install matimo` 0.2.0 and with the TypeScript SDK. ## The flow 1. The agent calls `matimo_create_tool` with a name and the tool YAML. 2. The tool is written as a **draft**: `approvalState: pending`, `riskLevel: high`. 3. It is **not runnable**. Calling it reports "not found". 4. A reviewer calls `matimo_approve_tool`, then `matimo_reload_tools`. 5. The tool now loads and runs. ```typescript const matimo = await MatimoInstance.init({ toolPaths: ['./matimo-tools'], autoDiscover: true, onApproval: async (request) => askReviewer(request), }); const created = await matimo.execute('matimo_create_tool', { name: 'fetch-weather', yaml_content }); // created.path shows where it was written; that directory must be in toolPaths await matimo.execute('matimo_approve_tool', { name: 'fetch-weather' }); await matimo.execute('matimo_reload_tools', {}); await matimo.execute('fetch-weather', { city: 'Paris' }); ``` ```python matimo = await Matimo.init( tool_paths=["./matimo-tools"], auto_discover=True, on_approval=on_approval, ) await matimo.execute("matimo_create_tool", {"name": "fetch-weather", "yaml_content": yaml_content}) await matimo.execute("matimo_approve_tool", {"name": "fetch-weather"}) await matimo.execute("matimo_reload_tools", {}) await matimo.execute("fetch-weather", {"city": "Paris"}) ``` ## Who may approve When your host passes a context that carries the agent's `agentId`, the agent that created a tool cannot approve it, even with the `admin` role. A different agent with the `admin` role can. When your host passes no context, nothing checks who is calling, and only the approval prompt protects the step. Roles are strings your host puts in the context. Matimo has no user accounts. See [Where roles come from](https://matimo.dev/docs/api-reference/policy-and-lifecycle.md#where-roles-come-from). ## Things that trip people up - Set a stable `MATIMO_APPROVAL_SECRET`. Approve and reload in the same process, or approvals will not match. - Keep the directory the tool was written to in `toolPaths` (`tool_paths`). Otherwise the new tool reports not found after reload. - Approving is itself a call that needs approval. With no `onApproval`, it fails closed. - Editing an approved tool's definition invalidates its approval. It must be approved again. ## Read next - [Meta-Tools reference](https://matimo.dev/docs/api-reference/meta-tools.md) - [Policy Engine and Tool Lifecycle](https://matimo.dev/docs/api-reference/policy-and-lifecycle.md) - [Skills](https://matimo.dev/docs/skills/skills.md): let an agent write and load knowledge files too --- # API Reference: Complete SDK Source: https://matimo.dev/docs/api-reference/sdk/ Complete reference for the Matimo SDK in **TypeScript** and **Python**. For a simpler introduction, see [Quick Start](https://matimo.dev/docs/getting-started/quick-start.md) or [SDK Patterns](https://matimo.dev/docs/user-guide/sdk-patterns.md). ## Table of Contents ### TypeScript SDK (`MatimoInstance`) - [init()](#initoptions) - [execute()](#executetoolname-params-options) - [getRequiredCredentials()](#getrequiredcredentialstoolname) - [listTools()](#listtools) - [getTool()](#gettoolname) - [searchTools()](#searchtoolsquery) - [Decorators](#decorators) - [LangChain Integration](#langchain-integration) - [Error Handling](#error-handling) - [Types](#types) ### Python SDK (`Matimo`) - [Matimo.init()](#python-init) - [matimo.execute()](#python-execute) - [matimo.list_tools()](#python-list-tools) - [matimo.get_tool()](#python-get-tool) - [matimo.search_tools()](#python-search-tools) - [matimo.reload()](#python-reload) - [matimo.list_skills() / semantic_search_skills()](#python-list-skills) - [Decorator pattern](#python-decorators) - [LangChain integration](#python-langchain) - [CrewAI integration](#python-crewai) - [Logging](#python-logging) - [Error handling](#python-errors) --- ## TypeScript SDK: `MatimoInstance` Main entry point for the Matimo TypeScript SDK. ### `init(options?)` Initialize Matimo with tools from specified paths or auto-discovery. **Signature:** ```typescript static async init(options?: InitOptions | string): Promise ``` **Parameters:** - `options` (InitOptions | string, optional), a single directory path, or: | Option | Type | Description | |--------|------|-------------| | `toolPaths` | `string[]` | Tool directories to load | | `autoDiscover` | `boolean` | Also load tools (and their skills) from installed `@matimo/*` packages | | `skillPaths` | `string[]` | Directories of `SKILL.md` skills. Matimo's core skills are always added | | `policy` | `PolicyEngine` | Custom policy engine; overrides `policyConfig` and `policyFile` | | `policyConfig` | `PolicyConfig` | Options for the built-in `DefaultPolicyEngine` | | `policyFile` | `string` | Path to a `policy.yaml` | | `trustedPaths` / `untrustedPaths` | `string[]` | Which directories hold developer tools and agent-written tools; untrusted tools must pass the content rules | | `governanceMode` | `'secure' \| 'legacy'` | `'secure'` (default): HTTP DELETE and command tools ask on every call. `'legacy'`: pre-0.2.0 defaults | | `onApproval` | `ApprovalCallback` | This instance's reviewer for calls that need approval ([Approval System](https://matimo.dev/docs/api-reference/approval-system.md)) | | `onHITL` | `HITLCallback` | Decides calls quarantined by `policyConfig.enableHITL` | | `hitlTimeoutMs` | `number` | Reject a quarantined call when `onHITL` takes longer | | `onEvent` | `MatimoEventHandler` | Receives every governance and execution event ([Types](https://matimo.dev/docs/api-reference/types.md)) | | `auditSink` | `AuditSink` | Durable destination for the same events, e.g. `new JsonlFileSink(path)` | | `approvalSecret` / `approvalDir` / `approvalTtlSeconds` | | Signing secret (else `MATIMO_APPROVAL_SECRET`, else ephemeral), location (default: cwd) and expiry of `matimo_approve_tool` approvals | | `defaultMaxResponseSize` | `number` | Response-size cap in bytes for tools that don't set `output_schema.max_response_size` (built-in default 262144 / 256 KB, see [Response Size Guardrail](https://matimo.dev/docs/architecture/overview.md#tool-execution-flow)) | | `defaultSkillWriteDir` | `string` | Where `matimo_create_skill` writes when the caller gives no `target_dir` | | `logLevel` / `logFormat` | | Logger settings | `includeCore` is accepted but has no effect. **Returns:** `Promise` - Initialized instance ready to execute tools **Throws:** - `MatimoError(INVALID_SCHEMA)` - If tool definitions have invalid schema - `MatimoError(FILE_NOT_FOUND)` - If tools directory doesn't exist **Example:** ```typescript import { MatimoInstance } from 'matimo'; // Auto-discover tools from node_modules/@matimo/* packages const matimo = await MatimoInstance.init({ autoDiscover: true }); // Or specify custom tool paths const matimo = await MatimoInstance.init({ toolPaths: ['./tools'], }); // Backward compatibility - single directory (loads only ./tools, no built-in tools) const matimo = await MatimoInstance.init('./tools'); console.log(`Loaded ${matimo.listTools().length} tools`); ``` --- ### `execute(toolName, params, options?)` Execute a tool by name with parameters. **Signature:** ```typescript async execute( toolName: string, params: Record, options?: ExecuteOptions ): Promise interface ExecuteOptions { /** Execution timeout in milliseconds. */ timeout?: number; /** * Per-call credential overrides for multi-tenant use. * Keys must match the env-var names the tool references (e.g. `SLACK_BOT_TOKEN`). * When provided, they take precedence over `process.env` for that single call. * Values are never logged and are held in memory only for the duration of the call. */ credentials?: Record; /** Who is calling: checked by the policy engine (environment, roles, agentId). */ context?: PolicyContext; /** Skip the approval prompt for this call only; policy denials and HITL still apply. */ approved?: boolean; /** Reviewer for this call only; takes precedence over the instance's onApproval. */ onApproval?: ApprovalCallback; } ``` **Parameters:** - `toolName` (string, required) - Exact name of the tool to execute - `params` (object, required) - Tool parameters (must match tool's parameter schema) - `options.timeout` (number, optional) - Execution timeout in milliseconds - `options.credentials` (object, optional) - Per-call credential overrides (see Multi-tenant Usage below) - `options.context` (PolicyContext, optional) - The caller's identity, environment and roles, e.g. `{ agentId: 'agent-7', environment: 'production', roles: ['operator'] }`. Function tools receive it as `context.policyContext` - `options.approved` (boolean, optional) - The host already confirmed this call; skips only the approval prompt - `options.onApproval` (ApprovalCallback, optional) - Reviewer for this one call **Returns:** `Promise` - Tool result (validated against output schema). If the raw result exceeds the effective response-size cap (`output_schema.max_response_size`, else `defaultMaxResponseSize`, else a built-in 256 KB), it is truncated rather than rejected, see [Response Size Guardrail](https://matimo.dev/docs/architecture/overview.md#tool-execution-flow). **Throws:** - `MatimoError(TOOL_NOT_FOUND)` - If tool name doesn't exist - `MatimoError(INVALID_PARAMETER)` - If params don't match tool schema - `MatimoError(POLICY_DENIED)` - If the policy engine, a draft/production gate or a HITL reviewer refuses the call - `MatimoError(EXECUTION_FAILED)` - If the call needs approval and is declined or nobody can answer - `MatimoError(EXECUTION_FAILED)` - If tool execution fails - `MatimoError(AUTH_FAILED)` - If authentication fails - `MatimoError(TIMEOUT)` - If execution exceeds timeout **Example (single-tenant):** ```typescript import { MatimoInstance, MatimoError } from 'matimo'; const matimo = await MatimoInstance.init({ autoDiscover: true }); try { // Credentials read from process.env (SLACK_BOT_TOKEN, etc.) const result = await matimo.execute('calculator', { operation: 'add', a: 10, b: 5, }); console.log('Result:', result); // { result: 15 } const slackResult = await matimo.execute('slack-send-message', { channel: '#general', text: 'Hello', }); console.log('Message sent:', slackResult); } catch (error) { if (error instanceof MatimoError) { console.error(`Error [${error.code}]:`, error.message); } } ``` **Multi-tenant Usage:** Different tenants can supply their own credentials **per call** without touching `process.env`. This lets a single process serve many tenants safely. ```typescript import { MatimoInstance } from 'matimo'; const matimo = await MatimoInstance.init({ autoDiscover: true }); // Tenant A, uses their own Slack token await matimo.execute( 'slack-send-message', { channel: '#general', text: 'Hello from Tenant A' }, { credentials: { SLACK_BOT_TOKEN: 'xoxb-tenant-a-token' } } ); // Tenant B, same process, completely isolated credentials await matimo.execute( 'slack-send-message', { channel: '#general', text: 'Hello from Tenant B' }, { credentials: { SLACK_BOT_TOKEN: 'xoxb-tenant-b-token' } } ); // Timeout + credentials together await matimo.execute( 'github-create-issue', { repo: 'myorg/myrepo', title: 'Bug report' }, { timeout: 10_000, credentials: { GITHUB_TOKEN: 'ghp-tenant-c-token' }, } ); ``` **Credential key naming convention:** Credential keys must match the env-var names the tool's YAML definition references (e.g. `SLACK_BOT_TOKEN`, `GITHUB_TOKEN`). The credential value is resolved in this order for each placeholder found in the tool YAML: 1. `credentials[paramName]`: per-call override (highest priority) 2. `credentials[MATIMO_${paramName}]`: prefixed per-call override 3. `process.env[MATIMO_${paramName}]`: prefixed env var 4. `process.env[paramName]`: direct env var (lowest priority) **Security notes:** - Credential values are **never logged** by the Matimo SDK. - Credentials are **never persisted**, held in memory only for the life of the call. - `process.env` is **never modified**, each call's credentials are isolated. - For `command` tools, credentials are merged into the child-process environment (`{ ...process.env, ...credentials }`) and not leaked back to the parent process. --- ### `getRequiredCredentials(toolName)` Return the credential key names a tool needs, so callers know exactly what to put in `options.credentials` without reading the tool YAML. This is the primary discovery API for multi-tenant platforms: call this once when building your tenant-credential collection step, then pass the result directly to `execute()`. **Signature:** ```typescript getRequiredCredentials(toolName: string): string[] ``` **Parameters:** - `toolName` (string, required), Exact tool name **Returns:** `string[]`, Array of credential key names the tool requires (empty if the tool needs no auth) **Throws:** `MatimoError(TOOL_NOT_FOUND)` if the tool doesn't exist **Example:** ```typescript const matimo = await MatimoInstance.init({ autoDiscover: true }); // 1. Discover what credentials this tool needs const keys = matimo.getRequiredCredentials('slack-send-message'); console.log(keys); // → ['SLACK_BOT_TOKEN'] // 2. Collect those values from your secrets store / tenant config const credentials = Object.fromEntries( keys.map((key) => [key, tenant.vault.get(key)]) ); // 3. Execute with isolated per-tenant credentials await matimo.execute('slack-send-message', params, { credentials }); ``` **Multi-tool credential prep:** ```typescript // Build a credential map for every installed tool at startup const credentialManifest = Object.fromEntries( matimo.listTools().map((tool) => [ tool.name, matimo.getRequiredCredentials(tool.name), ]) ); // credentialManifest looks like: // { // 'slack-send-message': ['SLACK_BOT_TOKEN'], // 'github-create-issue': ['GITHUB_TOKEN'], // 'twilio-send-sms': ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN'], // } // When a request comes in for tenant X, collect only what that tool needs: async function runForTenant(toolName: string, params: Record, tenant: Tenant) { const keys = credentialManifest[toolName]; const credentials = Object.fromEntries(keys.map((k) => [k, tenant.secrets[k]])); return matimo.execute(toolName, params, { credentials }); } ``` --- ### `listTools()` Get all available tools. **Signature:** ```typescript listTools(): ToolDefinition[] ``` **Returns:** `ToolDefinition[]` - Array of all loaded tool definitions **Example:** ```typescript const matimo = await MatimoInstance.init({ autoDiscover: true }); const tools = matimo.listTools(); console.log(`Available tools (${tools.length}):`); tools.forEach((tool) => { console.log(` - ${tool.name}: ${tool.description}`); console.log(` Parameters: ${Object.keys(tool.parameters || {}).join(', ')}`); }); ``` --- ### `getTool(name)` Get a single tool definition by name. **Signature:** ```typescript getTool(name: string): ToolDefinition | undefined ``` **Parameters:** - `name` (string) - Exact tool name **Returns:** `ToolDefinition | undefined` - Tool definition if found, undefined otherwise **Example:** ```typescript const matimo = await MatimoInstance.init({ autoDiscover: true }); const slackTool = matimo.getTool('slack-send-message'); if (slackTool) { console.log('Tool:', slackTool.name); console.log('Description:', slackTool.description); console.log('Parameters:'); Object.entries(slackTool.parameters || {}).forEach(([name, param]) => { console.log(` - ${name}: ${param.type}${param.required ? ' (required)' : ''}`); }); } else { console.log('Tool not found'); } ``` --- ### `searchTools(query)` Search tools by name or description. **Signature:** ```typescript searchTools(query: string): ToolDefinition[] ``` **Parameters:** - `query` (string) - Search query (matched case-insensitively against name and description) **Returns:** `ToolDefinition[]` - Matching tools **Example:** ```typescript const matimo = await MatimoInstance.init({ autoDiscover: true }); // Find all Slack-related tools const slackTools = matimo.searchTools('slack'); console.log(`Found ${slackTools.length} Slack tools`); // Find email tools const emailTools = matimo.searchTools('email'); emailTools.forEach((tool) => console.log(` - ${tool.name}`)); ``` --- ## Decorators Use decorators for clean, declarative tool execution in class-based code. ### Governance and skills methods | TypeScript | Python | Description | |------------|--------|-------------| | `setApprovalCallback(cb)` | `set_approval_callback(cb)` | Set or clear the instance's approval callback (`null`/`None` falls back to the process-wide handler) | | `getGovernanceMode()` | `get_governance_mode()` | `'secure'` or `'legacy'` | | `getApprovalManifest()` | `get_approval_manifest()` | The manifest `matimo_approve_tool` records approvals in | | `reloadTools()` | `reload()` | Re-read tool directories; returns `{ loaded, removed, revalidated, rejected }` | | `setHITLCallback(cb)` | n/a | Set or clear the HITL callback (Python: pass `on_hitl` to `init()`) | | `reloadPolicy(configOrFile?)` | n/a | Swap the policy engine and re-check tools | | `getToolsByTag(tag)` | n/a | Tools carrying a tag | | `registerSkill(skill)` / `registerSkills([...])` | `register_skill(skill)` / `register_skills([...])` | Add skills from your own storage (dropped by the next `reloadSkills()`) | | `addSkillPath(dir)` + `reloadSkills()` | `add_skill_path(dir)` + `reload_skills()` | Mount another skills directory at runtime; returns `{ loaded, removed }` | | `getSkillSections(name)` | `get_skill_sections(name)` | Section headings and token estimates | | `getSkillContent(name, { sections, maxTokens, includePreamble, maxDepth })` | `get_skill_content(name, SkillContentOptions(...))` | Load part of a skill | | `buildSkillPromptContext(query, { topK })` | `build_skill_prompt_context(query, top_k=...)` | Relevant skills (TF-IDF), formatted for a system prompt | | `getDefaultSkillWriteDir()` | `get_default_skill_write_dir()` | Where `matimo_create_skill` writes by default | | `getSkillResource(name, path)` | n/a | Read a file bundled with a skill | Runnable demos of these, needing no API keys: `typescript/examples/tools/policy/approval-modes-demo.ts`, `skills/skills-registry-demo.ts`, and the Python twins under `python/examples/native/`. --- ### `@tool(toolName)` Class method decorator that automatically executes a tool when the method is called. **Signature:** ```typescript function tool(toolName: string): MethodDecorator; ``` **How it works:** 1. When decorated method is called, decorator intercepts the call 2. Method parameters are passed to `matimo.execute(toolName, params)` 3. Tool result is returned directly 4. Method body is never executed **Requirements:** - Global Matimo instance must be set: `setGlobalMatimoInstance(matimo)` - Tool name must match exactly (case-sensitive) - Method parameters must match tool parameters (by order or destructuring) **Example, Simple Tool Execution:** ```typescript import { tool, setGlobalMatimoInstance, MatimoInstance } from 'matimo'; const matimo = await MatimoInstance.init({ autoDiscover: true }); setGlobalMatimoInstance(matimo); class Calculator { @tool('calculator') async add(operation: string, a: number, b: number) { // Method body is ignored // Decorator passes (operation, a, b) to matimo.execute('calculator', {...}) } } const calc = new Calculator(); const result = await calc.add('add', 5, 3); console.log(result); // { result: 8 } ``` **Example, Slack Agent:** ```typescript import { tool, setGlobalMatimoInstance, MatimoInstance, MatimoError } from 'matimo'; const matimo = await MatimoInstance.init({ autoDiscover: true }); setGlobalMatimoInstance(matimo); class SlackAgent { @tool('slack-send-message') async sendMessage(channel: string, text: string) { // Decorator handles execution } @tool('slack_get_channel_history') async getHistory(channel: string, limit: number) { // Also handled by decorator } } try { const agent = new SlackAgent(); // These calls trigger matimo.execute() automatically await agent.sendMessage('#general', 'Hello world!'); const channelInfo = await agent.getChannel('general'); console.log('Channel:', channelInfo); } catch (error) { if (error instanceof MatimoError) { console.error(`Tool error [${error.code}]:`, error.message); } } ``` **Example, With Error Handling:** ```typescript import { tool, setGlobalMatimoInstance, MatimoInstance, MatimoError } from 'matimo'; const matimo = await MatimoInstance.init({ autoDiscover: true }); setGlobalMatimoInstance(matimo); class APIClient { @tool('api-call') async makeRequest(method: string, url: string, body?: string) { // Never runs, but provides type hints } } const client = new APIClient(); try { const response = await client.makeRequest('GET', 'https://api.example.com/users'); console.log('Response:', response); } catch (error) { if (error instanceof MatimoError) { switch (error.code) { case 'TOOL_NOT_FOUND': console.error('Tool not found'); break; case 'AUTH_FAILED': console.error('Authentication failed'); break; case 'EXECUTION_FAILED': console.error('Tool execution failed:', error.details); break; default: console.error('Unknown error:', error.message); } } } ``` --- ### `setGlobalMatimoInstance(instance)` Set the global Matimo instance for all decorators to use. **Signature:** ```typescript function setGlobalMatimoInstance(instance: MatimoInstance): void; ``` **Parameters:** - `instance` (MatimoInstance) - Initialized Matimo instance from `MatimoInstance.init()` **Note:** Must be called before using any `@tool` decorators. **Example:** ```typescript import { setGlobalMatimoInstance, MatimoInstance } from 'matimo'; // Initialize once const matimo = await MatimoInstance.init({ autoDiscover: true }); // Set globally for all decorators setGlobalMatimoInstance(matimo); // Now @tool decorators will use this instance ``` --- ## LangChain Integration Convert Matimo tools to LangChain tool format for AI agents. ### `convertToolsToLangChain(tools, matimo, secrets?, secretParamNames?)` Convert Matimo tools to LangChain tools whose calls go through `matimo.execute()`, with the same policy and approval checks as a direct call. **Signature:** ```typescript async function convertToolsToLangChain( tools: ToolDefinition[], matimo: MatimoInstance, secrets?: Record, secretParamNames?: Set ): Promise; ``` **Parameters:** - `tools` (ToolDefinition[], required) - The tools to expose; keep the list small (OpenAI accepts at most 128) - `matimo` (MatimoInstance, required) - Initialized Matimo instance; its `onApproval` answers approval requests - `secrets` (object, optional) - Values for secret parameters, keyed by parameter name - `secretParamNames` (Set, optional) - Parameter names to treat as secrets, in place of the keys of `secrets` Parameters whose names contain `TOKEN`, `KEY`, `SECRET` or `PASSWORD` are treated as secrets too: they are left out of the schema the model sees, and filled from `secrets`. Credentials in `{PLACEHOLDERS}` that no parameter declares are filled by `execute()` from `MATIMO_` or `` in the environment. **Returns:** `Promise` **Example:** ```typescript import { MatimoInstance, convertToolsToLangChain } from '@matimo/core'; import { ChatOpenAI } from '@langchain/openai'; import { createAgent } from 'langchain'; const matimo = await MatimoInstance.init({ autoDiscover: true, onApproval: async (request) => askUser(request), // asked before writes that need approval }); const tools = matimo.listTools().filter((t) => t.name.startsWith('slack_')); const langchainTools = await convertToolsToLangChain(tools, matimo); const agent = createAgent({ model: new ChatOpenAI({ model: 'gpt-4o-mini' }), tools: langchainTools, }); const response = await agent.invoke({ messages: [{ role: 'user', content: 'Send a message to #general saying hello' }], }); ``` For complete LangChain integration guide, see [LangChain Integration](https://matimo.dev/docs/framework-integrations/langchain.md). --- ## Error Handling All SDK errors are instances of `MatimoError` with structured error codes. ### MatimoError **Properties:** - `message` (string) - Human-readable error message - `code` (ErrorCode) - Machine-readable error code - `details` (object, optional) - Additional error context **Available Error Codes:** ```typescript enum ErrorCode { INVALID_SCHEMA = 'INVALID_SCHEMA', // Tool definition invalid, or a URL parameter is missing INVALID_PARAMETER = 'INVALID_PARAMETER', // A parameter value cannot be encoded VALIDATION_FAILED = 'VALIDATION_FAILED', TOOL_NOT_FOUND = 'TOOL_NOT_FOUND', // Tool name not found FILE_NOT_FOUND = 'FILE_NOT_FOUND', // Tool file not found EXECUTION_FAILED = 'EXECUTION_FAILED', // Tool error, a non-2xx HTTP response, or approval refused AUTH_FAILED = 'AUTH_FAILED', // Missing credentials, or HTTP 401/403 RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED', // HTTP 429 TIMEOUT = 'TIMEOUT', // Execution timeout NETWORK_ERROR = 'NETWORK_ERROR', // DNS failure, connection refused POLICY_DENIED = 'POLICY_DENIED', // Denied by policy, or quarantined and not approved POLICY_TIER_BLOCKED = 'POLICY_TIER_BLOCKED', UNKNOWN_ERROR = 'UNKNOWN_ERROR', } ``` **Example:** ```typescript import { MatimoInstance, MatimoError } from 'matimo'; const matimo = await MatimoInstance.init({ autoDiscover: true }); try { await matimo.execute('unknown-tool', {}); } catch (error) { if (error instanceof MatimoError) { console.error(`[${error.code}] ${error.message}`); // Handle specific errors if (error.code === 'TOOL_NOT_FOUND') { console.error( 'Available tools:', matimo.listTools().map((t) => t.name) ); } // View additional context if (error.details) { console.error('Details:', error.details); } } } ``` --- ## Types Complete TypeScript type definitions. ### ToolDefinition ```typescript interface ToolDefinition { name: string; // Unique tool name version: string; // Semantic version description: string; // Tool description parameters?: Record; // Tool parameters execution: HttpExecution | FunctionExecution | CommandExecution; // How to execute output_schema?: Record; // Response schema (Zod) authentication?: AuthConfig; // Auth configuration examples?: Example[]; // Usage examples requires_approval?: boolean; // Whether execution needs human approval risk?: 'low' | 'medium' | 'high' | 'critical'; // Self-declared risk (can only raise the automatically-computed level, never lower it) status?: 'draft' | 'approved' | 'deprecated'; // Lifecycle status, see POLICY_AND_LIFECYCLE.md tags?: string[]; // Free-form categorization tags deprecated?: boolean; deprecation_message?: string; } ``` ### Parameter ```typescript interface Parameter { type: string; // 'string', 'number', 'boolean', etc. required?: boolean; // Required flag description?: string; // Parameter description enum?: (string | number)[]; // Allowed values default?: unknown; // Default value } ``` ### Execution types ```typescript type Execution = | { type: 'http'; method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; url: string; headers?: Record; body?: unknown; query_params?: Record; parameter_encoding?: ParameterEncodingConfig[]; timeout?: number; // ms } | { type: 'function'; code: string; // module path, relative to the YAML file timeout?: number; } | { type: 'command'; command: string; args?: string[]; timeout?: number; }; ``` Exported as `HttpExecution`, `FunctionExecution` and `CommandExecution`. See [Types](https://matimo.dev/docs/api-reference/types.md#execution-types). ### AuthConfig ```typescript interface AuthConfig { type: 'api_key' | 'bearer' | 'basic' | 'oauth2'; location?: 'header' | 'query' | 'body'; // For api_key/bearer name?: string; // Header/param name provider?: string; // For oauth2 } ``` --- ## Python SDK: `Matimo` API Complete Python SDK reference. Mirrors the TypeScript `MatimoInstance` API with Python conventions (`snake_case`, `asyncio`). ### Table of Contents (Python) - [`Matimo.init()`](#python-init) - [`matimo.execute()`](#python-execute) - [`matimo.list_tools()`](#python-list-tools) - [`matimo.get_tool()`](#python-get-tool) - [`matimo.search_tools()`](#python-search-tools) - [`matimo.reload()`](#python-reload) - [`matimo.list_skills()`](#python-list-skills) - [`matimo.semantic_search_skills()`](#python-semantic-search-skills) - [Decorator pattern](#python-decorators) - [LangChain integration](#python-langchain) - [CrewAI integration](#python-crewai) - [Logging](#python-logging) - [Error handling](#python-errors) --- ### `Matimo.init()` ```python @classmethod async def init( cls, tool_paths: str | list[str] | None = None, *, auto_discover: bool = False, skill_paths: list[str] | None = None, policy: PolicyEngine | None = None, policy_config: PolicyConfig | None = None, policy_file: str | None = None, trusted_paths: list[str] | None = None, untrusted_paths: list[str] | None = None, approval_secret: str | None = None, approval_dir: str | None = None, approval_ttl_seconds: int | None = None, on_event: MatimoEventHandler | None = None, audit_sink: AuditSink | None = None, on_hitl: HITLCallback | None = None, on_approval: ApprovalCallback | None = None, governance_mode: GovernanceMode | None = None, hitl_timeout_ms: int | None = None, log_level: str | None = None, log_format: str | None = None, default_max_response_size: int | None = None, default_skill_write_dir: str | None = None, ) -> 'Matimo' ``` Every option is a direct keyword-only argument on `init()` itself, there is no `InitOptions` wrapper object in Python (unlike an options-bag pattern you may have seen elsewhere). **Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `tool_paths` | `str \| list[str]` | `None` | Explicit tool directories to load | | `auto_discover` | `bool` | `False` | Load tools from installed `matimo-*` packages | | `skill_paths` | `list[str]` | `None` | Directories containing SKILL.md files | | `policy` | `PolicyEngine` | `None` | Custom policy engine instance. Mutually exclusive with `policy_config`/`policy_file` | | `policy_config` | `PolicyConfig` | `None` | Shorthand to build a `DefaultPolicyEngine` with this config | | `policy_file` | `str` | `None` | Load policy from a YAML file path | | `trusted_paths` | `list[str]` | `None` | Paths considered developer-authored (skip content validation) | | `untrusted_paths` | `list[str]` | `None` | Paths requiring stricter content validation | | `approval_secret` | `str` | `None` | HMAC secret for the approval manifest. Overrides `MATIMO_APPROVAL_SECRET` env | | `approval_dir` | `str` | `None` | Directory for `.matimo-approvals.json`. Defaults to the current working directory | | `approval_ttl_seconds` | `int` | `None` | Approval expiry in seconds. `None` means approvals never expire | | `on_event` | `Callable` | `None` | Event callback for audit/lifecycle events (dicts with snake_case keys) | | `audit_sink` | `AuditSink` | `None` | Durable destination for the same events, e.g. `JsonlFileSink(path)` | | `on_hitl` | `async Callable[[HITLRequest], bool]` | `None` | Decides calls quarantined by `policy_config.enable_hitl` | | `on_approval` | `async Callable[[ApprovalRequest], bool]` | `None` | This instance's reviewer for calls that need approval | | `governance_mode` | `'secure' \| 'legacy'` | `None` (secure) | `'legacy'` restores the pre-0.2.0 approval defaults | | `hitl_timeout_ms` | `int` | `None` | Timeout for the HITL callback; `None` waits indefinitely | | `log_level` | `str` | `None` | `'debug' \| 'info' \| 'warn' \| 'error' \| 'silent'` (resolved from env/defaults when unset) | | `log_format` | `str` | `None` | `'simple' \| 'json'` (resolved from env/defaults when unset) | | `default_skill_write_dir` | `str` | `None` | Where `matimo_create_skill` writes when the caller gives no `target_dir` | | `default_max_response_size` | `int` | `None` | Instance-wide response-size cap in bytes, applied to every tool call via `execute()` unless that tool's own `output_schema.max_response_size` overrides it (falls back to a built-in 262144 / 256 KB when unset, see [Response Size Guardrail](https://matimo.dev/docs/architecture/overview.md#tool-execution-flow)) | If no `policy`/`policy_config`/`policy_file` is given, `init()` always constructs a `DefaultPolicyEngine()`, a zero-config Python instance is never left ungated (this always was Python's behavior; the equivalent TypeScript `MatimoInstance.init()` was fixed to match it). **Examples:** ```python import asyncio from matimo import ApprovalRequest, Matimo, PolicyConfig # Simplest: load from a directory matimo = await Matimo.init('./tools') # All installed provider packages matimo = await Matimo.init(auto_discover=True) # Custom tools + auto-discover + policy matimo = await Matimo.init( tool_paths=['./tools'], auto_discover=True, untrusted_paths=['./tools'], policy_config=PolicyConfig( allowed_domains=['api.example.com'], allow_command_tools=False, ), log_level='debug', ) # With an approval callback + skills async def my_approval_callback(request: ApprovalRequest) -> bool: return input(f"Approve {request.tool_name}? (y/n): ").lower() == 'y' matimo = await Matimo.init( './tools', auto_discover=True, skill_paths=['./skills'], on_approval=my_approval_callback, ) ``` --- ### `matimo.execute()` ```python async def execute( self, tool_name: str, params: dict[str, object], *, credentials: dict[str, str] | None = None, context: PolicyContext | None = None, approved: bool = False, on_approval: ApprovalCallback | None = None, ) -> object ``` `context`, `approved` and `on_approval` behave as in TypeScript: the caller's identity and roles, skip-the-prompt for a call the host already confirmed, and a reviewer for this call only. Execute a tool by name. Raises `MatimoError` on failure. If the raw result exceeds the effective response-size cap (`output_schema.max_response_size`, else `default_max_response_size`, else a built-in 256 KB), it's truncated rather than rejected, see [Response Size Guardrail](https://matimo.dev/docs/architecture/overview.md#tool-execution-flow). ```python from matimo import Matimo, MatimoError matimo = await Matimo.init(auto_discover=True) # Basic call result = await matimo.execute('calculator', {'operation': 'add', 'a': 10, 'b': 5}) print(result) # {'result': 15.0} # Per-call credential override (multi-tenant) result = await matimo.execute( 'slack_send_channel_message', {'channel': '#general', 'text': 'Hello'}, credentials={'SLACK_BOT_TOKEN': tenant_token}, ) # Error handling try: result = await matimo.execute('unknown_tool', {}) except MatimoError as e: print(f"[{e.code}] {e}") if e.details: print("Details:", e.details) ``` --- ### `matimo.list_tools()` ```python def list_tools(self) -> list[ToolDefinition] ``` Return all currently registered tools as `ToolDefinition` objects. ```python tools = matimo.list_tools() print(f"Loaded {len(tools)} tools") for tool in tools: print(f" {tool.name} v{tool.version}, {tool.description}") ``` --- ### `matimo.get_tool()` ```python def get_tool(self, name: str) -> ToolDefinition | None ``` ```python tool_def = matimo.get_tool('slack_send_channel_message') if tool_def: for param_name, param in (tool_def.parameters or {}).items(): print(f" {param_name}: {param.type}{'*' if param.required else ''}") ``` --- ### `matimo.search_tools()` ```python def search_tools(self, query: str) -> list[ToolDefinition] ``` Case-insensitive substring search across tool `name` and `description`. ```python slack_tools = matimo.search_tools('slack') email_tools = matimo.search_tools('email') ``` --- ### `matimo.reload()` ```python async def reload(self) -> ReloadResult ``` Hot-reload all tools from their source paths. Untrusted tools are re-validated against the active policy, already-approved tools are checked with the looser `can_reload()` gate, everything else with the stricter `can_create()` gate (see POLICY_AND_LIFECYCLE.md). ```python result = await matimo.reload() print(f"Loaded: {result.loaded}, removed: {result.removed}, rejected: {result.rejected}") ``` `ReloadResult` has `loaded`, `removed`, `revalidated`, `rejected`, and `rolled_back` fields (mirroring TypeScript's `ReloadResult`). Note: unlike the TypeScript SDK, which snapshots the registry and restores it on a mid-load I/O failure, setting `rolledBack: true`, Python's `reload()` does not yet implement that snapshot/restore behavior, so `rolled_back` is currently always `False`. --- ### `matimo.list_skills()` and `matimo.semantic_search_skills()` ```python def list_skills(self) -> list[SkillDefinition] async def semantic_search_skills( self, query: str, limit: int = 5, min_score: float = 0.0, ) -> list[SemanticSearchResult] ``` ```python # List all skills skills = matimo.list_skills() for skill in skills: print(f" {skill.name}: {skill.description}") # Semantic search (TF-IDF) results = await matimo.semantic_search_skills('rate limiting and retries', limit=3) for r in results: print(f" {r.name} (score: {r.score:.3f})") ``` For higher-level helpers see [LangChain Skills Integration](https://matimo.dev/docs/framework-integrations/langchain.md#skills-integration-non-mcp). --- ### Python Decorators ```python from matimo import tool, set_global_matimo_instance, Matimo matimo = await Matimo.init(auto_discover=True) set_global_matimo_instance(matimo) class MyAgent: @tool('slack_send_channel_message') async def send(self, channel: str, text: str) -> object: ... # body never runs; decorator calls matimo.execute() @tool('calculator') async def calc(self, operation: str, a: float, b: float) -> object: ... agent = MyAgent() result = await agent.calc('add', 10, 5) # → {'result': 15.0} ``` --- ### Python LangChain Integration ```python from matimo import Matimo, convert_tools_to_langchain from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage, AIMessage, ToolMessage 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} # ReAct loop messages = [HumanMessage(content='List Slack channels')] for _ in range(10): response: AIMessage = await llm.ainvoke(messages) messages.append(response) if not response.tool_calls: print(response.content) break for call in response.tool_calls: tool_result = await tool_map[call['name']].ainvoke(call['args']) messages.append(ToolMessage(tool_call_id=call['id'], content=str(tool_result))) ``` **OpenAI 128-tool hard limit:** When `auto_discover=True` loads 153 tools (core plus every native provider), bind only the subset you need: ```python # Keep matimo_* meta-tools + specific providers _LIMIT = 128 def cap_tools(tools, priority_names=None): if len(tools) <= _LIMIT: return tools pri = set(priority_names or []) prioritized = [t for t in tools if t.name in pri] rest = [t for t in tools if t.name not in pri] return (prioritized + rest)[:_LIMIT] matimo_names = [t.name for t in lc_tools if t.name.startswith('matimo_')] lc_tools_capped = cap_tools(lc_tools, priority_names=matimo_names) llm = ChatOpenAI(model='gpt-4o-mini').bind_tools(lc_tools_capped) ``` See [LangChain Integration](https://matimo.dev/docs/framework-integrations/langchain.md) for the full guide. --- ### Python CrewAI Integration ```python from crewai import Agent, Task, Crew from langchain_openai import ChatOpenAI from matimo import Matimo, convert_tools_to_crewai matimo = await Matimo.init(auto_discover=True) tools = convert_tools_to_crewai(matimo.list_tools(), matimo) llm = ChatOpenAI(model='gpt-4o-mini') agent = Agent(role='Slack Manager', goal='Send messages', tools=tools, llm=llm) task = Task(description='Send a hello message to #general', agent=agent) crew = Crew(agents=[agent], tasks=[task]) result = crew.kickoff() ``` See [CrewAI Integration](https://matimo.dev/docs/framework-integrations/crewai.md) for the full guide. --- ### Python Logging ```python from matimo.logging import setup_logger, get_global_matimo_logger, set_global_matimo_logger # Simple text format (development) logger = setup_logger(level='debug', log_format='simple') logger.info('Starting', component='my-agent') # JSON structured format (production) logger = setup_logger(level='info', log_format='json') logger.warn('High latency', latency_ms=1200, tool='slack_send_channel_message') # Global singleton global_logger = get_global_matimo_logger() global_logger.error('Failed', code='EXECUTION_FAILED') # SDK logger access (all SDK internal logs use this) matimo = await Matimo.init('./tools', log_level='debug', log_format='simple') matimo._logger.debug('Custom debug message') # Silent mode (useful for tests) setup_logger(level='silent') ``` Log levels: `debug | info | warn | error | silent` Formats: `simple` (human-readable) | `json` (structured, for log aggregators) --- ### Python Error Handling ```python from matimo import MatimoError from matimo.errors import ErrorCode try: result = await matimo.execute('unknown_tool', {}) except MatimoError as e: print(f"[{e.code}] {e}") # e.code is a string matching ErrorCode enum values if e.code == ErrorCode.TOOL_NOT_FOUND: available = [t.name for t in matimo.list_tools()] print("Available:", available[:5]) elif e.code == ErrorCode.EXECUTION_FAILED: print("Details:", e.details) ``` **Error codes (same in TypeScript and Python):** | Code | Description | |------|-------------| | `INVALID_SCHEMA` | Tool definition YAML failed Pydantic validation | | `TOOL_NOT_FOUND` | No tool with that name in the registry | | `PARAMETER_VALIDATION` | Provided params don't match the tool's schema | | `EXECUTION_FAILED` | Tool ran but returned an error or failed output validation | | `AUTH_FAILED` | Missing or invalid credentials | | `TIMEOUT` | Execution exceeded timeout (HTTP executor) | | `FILE_NOT_FOUND` | Tool definition file missing | | `POLICY_BLOCKED` | PolicyEngine rejected the tool (blocked/deprecated) | | `POLICY_PENDING` | Tool requires HITL approval before execution | --- ## See Also - [Quick Start](https://matimo.dev/docs/getting-started/quick-start.md): 5-minute guide - [SDK Patterns](https://matimo.dev/docs/user-guide/sdk-patterns.md): Factory, Decorator, LangChain patterns - [LangChain Integration](https://matimo.dev/docs/framework-integrations/langchain.md): AI agent integration - [Architecture Overview](https://matimo.dev/docs/architecture/overview.md): System design --- # Policy Engine & Tool Lifecycle Guide Source: https://matimo.dev/docs/api-reference/policy-and-lifecycle/ > Complete developer and agent guide for Matimo's policy engine, tool creation, approval flow, hot-reload, and MCP integration. ## Table of Contents - [When to Use the Policy Engine](#when-to-use-the-policy-engine) - [Decision Guide](#decision-guide) - [Use Cases by Policy Feature](#use-cases-by-policy-feature) - [Benefits: Policy vs No Policy](#benefits-policy-vs-no-policy) - [Overview](#overview) - [Quick Start](#quick-start) - [Option 1: Load Policy from YAML File (Recommended for Teams)](#option-1-load-policy-from-yaml-file-recommended-for-teams) - [Option 2: Inline Policy Config (Development)](#option-2-inline-policy-config-development) - [Option 3: Custom PolicyEngine (Advanced)](#option-3-custom-policyengine-advanced) - [Policy Loading & Initialization](#policy-loading--initialization) - [Policy Configuration](#policy-configuration) - [PolicyConfig YAML File Format](#policyconfig-yaml-file-format) - [PolicyConfig Options (TypeScript)](#policyconfig-options-typescript) - [InitOptions (Full Configuration)](#initoptions-full-configuration) - [Execution Events](#execution-events) - [Audit Sink](#audit-sink) - [Immutability](#immutability) - [Content Validator](#content-validator) - [How It Integrates with Policy](#how-it-integrates-with-policy) - [9 Security Rules](#9-security-rules) - [Violation Severities](#violation-severities) - [Using validateToolContent()](#using-validatetoolcontent) - [Risk Classification](#risk-classification) - [Tool Lifecycle](#tool-lifecycle) - [Step 1: Create a Tool](#step-1-create-a-tool) - [Step 2: Approve a Tool](#step-2-approve-a-tool) - [Step 3: Reload Tools](#step-3-reload-tools) - [Step 4: Use the Tool](#step-4-use-the-tool) - [Full Lifecycle Example](#full-lifecycle-example) - [Approval System](#approval-system) - [How Approval Works](#how-approval-works) - [Interactive Terminal Approval](#interactive-terminal-approval) - [Tests and CI](#tests-and-ci) - [Pre-Approved Patterns](#pre-approved-patterns) - [Session Whitelisting](#session-whitelisting) - [MCP Approval Flow](#mcp-approval-flow) - [HITL Quarantine](#hitl-quarantine) - [How Quarantine Works](#how-quarantine-works) - [Enabling HITL](#enabling-hitl) - [HITLCallback & HITLRequest](#hitlcallback--hitlrequest) - [Resolution Flow](#resolution-flow) - [Quarantine Events](#quarantine-events) - [Quarantine Risk Levels](#quarantine-risk-levels) - [Policy Hot-Reload](#policy-hot-reload) - [reloadPolicy()](#reloadpolicy) - [parsePolicyFile()](#parsepolicyfile) - [Hot-Reload Events](#hot-reload-events) - [Integrity & Tamper Detection](#integrity--tamper-detection) - [SHA-256 Integrity Tracking](#sha-256-integrity-tracking) - [HMAC Approval Manifest](#hmac-approval-manifest) - [RBAC & Access Control](#rbac--access-control) - [Audit Events](#audit-events) - [MCP Integration](#mcp-integration) - [MCP + Policy Engine](#mcp--policy-engine) - [MCP + Tool Lifecycle](#mcp--tool-lifecycle) - [LangChain Agent Integration](#langchain-agent-integration) - [Setup](#setup) - [Full Lifecycle from LangChain Agent](#full-lifecycle-from-langchain-agent) - [API Reference](#api-reference) - [MatimoInstance](#matimoinstance) - [ReloadResult](#reloadresult) - [Policy Exports](#policy-exports) - [Examples](#examples) - [Policy Demo (Full 11-Mission Autonomous Agent)](#policy-demo-full-11-mission-autonomous-agent) - [Minimal Policy Setup](#minimal-policy-setup) - [Interactive Approval with Whitelist](#interactive-approval-with-whitelist) - [MCP Server with Policy](#mcp-server-with-policy) - [Python SDK, Policy & Lifecycle](#python-sdk-policy--lifecycle) - [Quick Start (Python)](#quick-start-python) - [Approval and HITL Callbacks](#approval-and-hitl-callbacks) - [Policy Events (Python)](#policy-events-python) - [Risk Classification (Python)](#risk-classification-python) - [Custom PolicyEngine (Python)](#custom-policyengine-python) - [Tool Lifecycle (Python)](#tool-lifecycle-python) - [Upgrading to 0.2.0](#upgrading-to-020) - [Quick path](#quick-path) - [New default: DELETE and command tools ask before every call](#new-default-delete-and-command-tools-ask-before-every-call) - [Fixes that change behaviour (both modes)](#fixes-that-change-behaviour-both-modes) - [Events](#events) - [Tool authoring (validators)](#tool-authoring-validators) - [New, additive](#new-additive) - [See Also](#see-also) ## When to Use the Policy Engine ### Decision Guide | Situation | Recommendation | |-----------|-----------------| | Agents can create/run tools dynamically (LangChain, MCP) | Always enable policy, agents must not execute arbitrary code | | Fixed tool set, no agent-created tools | Optional, policy is still best practice but not critical | | Production deployment | Load policy from `policy.yaml` file, version-controlled, auditable | | Development / local testing | Inline `policyConfig` is fine for quick iteration | | Multiple environments (dev/staging/prod) | Use `policyFile: process.env.POLICY_FILE` with environment-specific files | | LLM must call shell commands | Never, `allowCommandTools: false` always | | LLM must run arbitrary JS functions | Never, `allowFunctionTools: false` always | ### Use Cases by Policy Feature **`allowedDomains`**, Use when agents may create HTTP tools ```yaml # Lock agents to only call known-safe APIs allowedDomains: - api.github.com - api.slack.com - jsonplaceholder.typicode.com # Without this: an agent could create a tool targeting internal infrastructure ``` **`allowedCredentials`**, Use when agents handle auth tokens ```yaml # Prevent credential theft: agents can't reference env vars you haven't allowed allowedCredentials: - GITHUB_TOKEN - SLACK_BOT_TOKEN # Without this: agent could create a tool that exfiltrates OPENAI_API_KEY or AWS_SECRET ``` **`protectedNamespaces`**, Use to prevent namespace hijacking ```yaml # Agents cannot create tools named matimo_backdoor or company_prod_delete protectedNamespaces: - matimo_ - company_prod_ ``` **`enableHITL` + `quarantineRiskLevels`**, Use for medium/high-risk agentic workflows ```yaml # Human reviews any tool classified as medium or high risk before it executes enableHITL: true quarantineRiskLevels: - medium - high # Without this: agent will auto-execute POST/PUT/DELETE tools without human review ``` **`untrustedPaths`**, Always set when loading agent-created tools ```typescript await MatimoInstance.init({ autoDiscover: true, toolPaths: [agentToolsDir], // Where agent writes tools untrustedPaths: [agentToolsDir], // ← Mark same dir as untrusted → runs 9 security rules }); // Without untrustedPaths: agent-created tools skip validation entirely ``` ### Benefits: Policy vs No Policy | Risk | Without Policy | With Policy | |------|:--------------:|:-----------:| | Agent creates shell command tool | Executes ✗ | Blocked at `critical` | | Agent targets internal IP (SSRF) | Potential data leak ✗ | Blocked at `critical` | | Agent uses unapproved credential | Token theft possible ✗ | Blocked at `high` | | Agent names tool `matimo_backdoor` | Namespace collision ✗ | Blocked at `high` | | Medium-risk POST tool auto-executes | No oversight ✗ | HITL quarantine | | Policy changed by agent at runtime | Possible ✗ | `Object.freeze()` prevents it | --- ## Overview The Matimo Policy Engine provides defense-in-depth security for AI agent tool usage: ![The agent calls matimo.execute, the policy gate checks it with canExecute, the approval handler, the content validator, the risk classifier, the integrity tracker and the HMAC approval manifest, and an allowed call reaches the tool executor.](../../../../assets/diagrams/policy-gate-light.svg) ![The agent calls matimo.execute, the policy gate checks it with canExecute, the approval handler, the content validator, the risk classifier, the integrity tracker and the HMAC approval manifest, and an allowed call reaches the tool executor.](../../../../assets/diagrams/policy-gate-dark.svg) **Key principles:** 1. **Developer defines policy at deploy time**: agents cannot modify it 2. **Policy is Object.freeze()'d** after initialization, immutable at runtime 3. **All untrusted tools** are validated against the content rules 4. **Every decision** is logged as a structured audit event 5. **Deterministic**: same input always produces the same policy decision --- ## Quick Start ### Option 1: Load Policy from YAML File (Recommended for Teams) Create `policy.yaml`: ```yaml allowedDomains: - api.github.com - api.slack.com allowedHttpMethods: - GET - POST allowCommandTools: false allowFunctionTools: false protectedNamespaces: - matimo_ ``` Then initialize: ```typescript import { MatimoInstance } from 'matimo'; const matimo = await MatimoInstance.init({ toolPaths: ['./tools'], policyFile: './policy.yaml', // ← Load from file untrustedPaths: ['./agent-tools'], // Tools here get validated }); console.log(matimo.hasPolicy()); // true await matimo.execute('my_tool', { query: 'hello' }); ``` **Advantages:** - Environment-specific configs (dev/staging/prod policies) - Version-controlled policy changes - Easy to audit policy decisions - No rebuild needed to change policy ### Option 2: Inline Policy Config (Development) ```typescript import { MatimoInstance } from 'matimo'; import type { PolicyConfig } from 'matimo'; const policyConfig: PolicyConfig = { allowedDomains: ['api.github.com', 'api.slack.com'], allowedHttpMethods: ['GET', 'POST'], allowCommandTools: false, allowFunctionTools: false, protectedNamespaces: ['matimo_'], }; const matimo = await MatimoInstance.init({ toolPaths: ['./tools'], policyConfig, untrustedPaths: ['./agent-tools'], }); ``` ### Option 3: Custom PolicyEngine (Advanced) ```typescript import { MatimoInstance } from 'matimo'; import type { PolicyEngine, PolicyContext, PolicyDecision } from 'matimo'; import type { ToolDefinition } from 'matimo'; class MyCustomPolicy implements PolicyEngine { canCreate(context: PolicyContext, tool: ToolDefinition): PolicyDecision { // Custom logic here return { allowed: true }; } canExecute(context: PolicyContext, tool: ToolDefinition): PolicyDecision { // Custom logic here return { allowed: true }; } filterForAgent(tools: ToolDefinition[], context: PolicyContext): ToolDefinition[] { return tools; // Custom filtering } } const matimo = await MatimoInstance.init({ toolPaths: ['./tools'], policy: new MyCustomPolicy(), }); ``` ### Policy Loading & Initialization Under the hood, when you pass `policyFile`, Matimo: 1. Reads the YAML file using [`loadPolicyFromFile()`](https://github.com/tallclub/matimo/blob/main/typescript/packages/core/src/policy/policy-loader.ts) 2. Validates it against a strict Zod schema 3. Creates a `DefaultPolicyEngine` with the config 4. **Freezes the policy** with `Object.freeze()`, immutable at runtime 5. Returns the initialized instance If the YAML is invalid (syntax error, schema violation, or file missing), an error is thrown: ``` Error: Policy file "./policy.yaml" is invalid: • allowedDomains: expected array, received string ``` --- ## Policy Configuration ### PolicyConfig YAML File Format When using `policyFile`, create a YAML file with the following structure: ```yaml # policy.yaml # Matimo Policy Configuration # # Governs what agent-created tools are permitted. # Developer-authored tools in trustedPaths are NOT subject to this policy. # Only agent-proposed tools in untrustedPaths go through validation. # HTTP domain allowlist # If set, agent-created HTTP tools may only target these domains. allowedDomains: - api.slack.com - api.github.com - api.openai.com - jsonplaceholder.typicode.com # Credential allowlist # Env-var names that agent-created tools are allowed to reference. # If omitted, any credential is allowed (not recommended for production). allowedCredentials: - SLACK_BOT_TOKEN - GITHUB_TOKEN - OPENAI_API_KEY # HTTP methods allowlist # Methods allowed for agent-created HTTP tools. # Default: ['GET', 'POST'] allowedHttpMethods: - GET - POST # - PUT # Uncomment to allow # - DELETE # Uncomment to allow # Shell execution permission # Whether agent-created tools may use execution type 'command'. # BLOCKED at TIER 3 regardless: only developer tools should use this. # Recommendation: always false allowCommandTools: false # Function execution permission # Whether agent-created tools may use execution type 'function'. # BLOCKED at TIER 3 regardless: only developer tools should use this. # Recommendation: always false allowFunctionTools: false # Protected namespaces # Tool name prefixes reserved for built-in / developer tools. # Agents cannot create tools with these prefixes. # Default: ['matimo_'] protectedNamespaces: - matimo_ # - internal_ # Uncomment to add your own # - company_prod_ # Can add as many as needed # Governance mode # Default approval behaviour for tools whose YAML doesn't set requires_approval. # secure: HTTP DELETE and command tools ask before every call (default) # legacy: the pre-0.2.0 behaviour, where they don't ask # See "Upgrading to 0.2.0" below. governanceMode: secure ``` **Notes:** - All fields are **optional**, conservative defaults are used if omitted - Empty arrays (`[]`) are different from `null`/omitted: - Omitted: Use default (e.g., GET/POST for methods) - Empty array: Allow nothing (most restrictive) - **Case sensitivity:** HTTP methods should be uppercase (GET, POST, etc.) - Comments (`#`) are allowed, standard YAML ### PolicyConfig Options (TypeScript) ```typescript interface PolicyConfig { /** Allowed domains for HTTP tools. Tools targeting other domains are rejected. */ allowedDomains?: string[]; /** Allowed HTTP methods. Default: ['GET', 'POST'] */ allowedHttpMethods?: string[]; /** Allow tools with execution.type: 'command'. Default: false */ allowCommandTools?: boolean; /** Allow tools with execution.type: 'function'. Default: false */ allowFunctionTools?: boolean; /** Reserved namespace prefixes. Default: ['matimo_'] */ protectedNamespaces?: string[]; /** Allowed credential/env var names for agent-created tools */ allowedCredentials?: string[]; /** * Enable quarantine/HITL. When true, `canCreate()` quarantines agent-proposed tools * whose risk is listed in `quarantineRiskLevels`, and `canExecute()` quarantines every * tool whose execution risk is at or above `hitlMinRiskLevel`. Default: false. */ enableHITL?: boolean; /** * Risk levels quarantined (instead of rejected) when an agent creates a tool. Its least * severe entry is also the execution threshold unless `hitlMinRiskLevel` is set. * Default: ['medium'] */ quarantineRiskLevels?: RiskLevel[]; /** Execution-time quarantine threshold. Default: least severe entry of quarantineRiskLevels. */ hitlMinRiskLevel?: RiskLevel; /** Seconds after which an approval expires and the tool must be re-approved. Default: never expires. */ approvalTtlSeconds?: number; /** * 'secure': HTTP DELETE and command tools without `requires_approval` ask before * every call. 'legacy': pre-0.2.0 defaults. InitOptions.governanceMode overrides it. * Default: 'secure'. */ governanceMode?: 'secure' | 'legacy'; } ``` ### InitOptions (Full Configuration) ```typescript const matimo = await MatimoInstance.init({ // Tool discovery toolPaths: ['./tools', './agent-tools'], // Explicit paths autoDiscover: true, // Auto-discover @matimo/* packages includeCore: true, // Include built-in core tools /*────────────── POLICY (Choose ONE) ──────────────*/ // Option 1: Load from policy.yaml (RECOMMENDED for production) policyFile: './policy.yaml', // Option 2: Inline policy config (development/simple cases) // policyConfig: { // allowedDomains: ['api.example.com'], // allowedHttpMethods: ['GET', 'POST'], // allowCommandTools: false, // allowFunctionTools: false, // protectedNamespaces: ['matimo_'], // enableHITL: true, // Enable quarantine flow // quarantineRiskLevels: ['medium', 'high'], // Risk levels eligible for quarantine // }, // Option 3: Custom PolicyEngine (advanced) // policy: new MyCustomPolicyEngine(), /*──────────────────────────────────────────────*/ untrustedPaths: ['./agent-tools'], // Agent tools → validated against policy trustedPaths: ['./tools'], // Developer tools → skip validation // Approval & Audit approvalSecret: process.env.MATIMO_APPROVAL_SECRET, // HMAC secret for signatures approvalDir: './approvals', // Where to store approvals onEvent: (event) => { // Audit trail handler console.log(`[${event.type}]`, event); }, auditSink: new JsonlFileSink('./logs/matimo-audit.jsonl'), // Hash-chained audit log // HITL Quarantine onHITL: async (request) => { // Human-in-the-loop callback console.log(`Quarantined: ${request.toolName} (${request.riskLevel})`); return await askHumanOperator(request); // Return true to approve }, // Logging logLevel: 'info', // 'silent' | 'error' | 'warn' | 'info' | 'debug' logFormat: 'json', // 'json' | 'simple' }); ``` **Policy Priority:** 1. If `policy` is provided → use it (highest priority) 2. Else if `policyFile` is provided → load and use it 3. Else if `policyConfig` is provided → use it 4. Else → use `DefaultPolicyEngine()` with conservative defaults **Recommendation for Production:** ```bash # Store policy.yaml in version control git add policy.yaml # Different policies per environment policy-dev.yaml policy-staging.yaml policy-prod.yaml # Load based on environment policyFile: process.env.POLICY_FILE || './policy.yaml' ``` > **Tip:** You can also pass a custom `PolicyEngine` implementation via the `policy` option instead of `policyConfig`. ### Execution Events Every call that passes the gates (policy, quarantine, approval) ends in exactly one of two events. A call a gate refuses gets that gate's event instead (`tool:execution_denied`, `tool:quarantine_rejected`, `tool:approval_denied`). | Event | When | Fields | |---|---|---| | `tool:executed` | the tool returned | `toolName`, `agentId`?, `traceId`, `durationMs`, `success`, `riskLevel`, `timestamp` | | `tool:execution_failed` | the tool threw | `toolName`, `agentId`?, `traceId`, `durationMs`, `riskLevel`, `errorCode`, `error`, `timestamp` | - `success` is `false` when the tool returned `{ success: false }` rather than throwing. - `durationMs` is time in the tool itself; approval waits are not counted. - `riskLevel` is the execution risk (`classifyExecutionRisk`). - The Python SDK emits the same events as dicts with snake_case keys (`tool_name`, `trace_id`, `duration_ms`, ...). Both SDKs are tested against [`conformance/events/execution-events.json`](https://github.com/tallclub/matimo/blob/main/conformance/events/execution-events.json). ### Audit Sink `onEvent` hands events to your code. To keep a durable record of the same events, pass an `AuditSink`. It runs alongside `onEvent`, and a sink that throws is logged but never fails the tool call. ```typescript import { MatimoInstance, JsonlFileSink, verifyAuditLog } from '@matimo/core'; const matimo = await MatimoInstance.init({ autoDiscover: true, auditSink: new JsonlFileSink('./logs/matimo-audit.jsonl'), }); verifyAuditLog('./logs/matimo-audit.jsonl'); // { valid: true, entries: 42 } ``` ```python from matimo import Matimo, JsonlFileSink, verify_audit_log matimo = await Matimo.init(auto_discover=True, audit_sink=JsonlFileSink("./logs/matimo-audit.jsonl")) verify_audit_log("./logs/matimo-audit.jsonl") # AuditLogVerification(valid=True, entries=42) ``` `JsonlFileSink` writes one line per event: ```json {"event":{...},"hash":"","prevHash":"","seq":7} ``` - `hash` is `sha256(prevHash + canonicalJson({seq, event}))`. Canonical JSON sorts keys at every level and has no whitespace. The first line's `prevHash` is 64 zeros. - Editing, deleting or reordering a line makes `verifyAuditLog` / `verify_audit_log` report the first bad line. Anyone who can write the file can also rewrite the whole chain, so ship the file (or its latest hash) somewhere append-only if you need tamper evidence against the host itself. - Opening an existing log continues its chain from the last line. - Values under secret-named keys (`password`, `token`, `apiKey`, `authorization`, `client_secret`, `cookie`, ...) are written as `[REDACTED]`. - A log written by either SDK verifies in the other. Both are tested against [`conformance/audit/hash-chain.json`](https://github.com/tallclub/matimo/blob/main/conformance/audit/hash-chain.json). - One `JsonlFileSink` per file per process. Two processes appending to the same file will fork the chain. For another destination (a database, a SIEM, a queue), implement `write(event)`. It may return a promise in TypeScript. ### Immutability After `MatimoInstance.init()`, the policy configuration is `Object.freeze()`'d: ```typescript // These would throw at runtime, policy is frozen matimo.policyConfig.allowCommandTools = true; // TypeError: Cannot assign matimo.policyConfig.allowedDomains.push('evil.com'); // TypeError: Cannot add ``` This ensures agents cannot weaken security at runtime. --- ## Content Validator ### How It Integrates with Policy The content validator is the enforcement engine for your policy: ![policy.yaml becomes a PolicyConfig. When an agent proposes a tool, the ContentValidator uses that config to check the 9 rules plus the policy settings, and the tool either passes or is blocked.](../../../../assets/diagrams/policy-validation-light.svg) ![policy.yaml becomes a PolicyConfig. When an agent proposes a tool, the ContentValidator uses that config to check the 9 rules plus the policy settings, and the tool either passes or is blocked.](../../../../assets/diagrams/policy-validation-dark.svg) The validator **automatically** uses your `allowedDomains`, `allowedHttpMethods`, `allowedCredentials`, and `protectedNamespaces` from the policy configuration. ### 9 Security Rules The content validator runs 9 deterministic rules against every untrusted tool definition. Each rule produces a violation with a severity level. | # | Rule ID | Severity | What It Checks | |---|---------|----------|----------------| | 1 | `no-function-execution` | **critical** | Blocks `execution.type: function` (arbitrary code execution) | | 2 | `no-command-execution` | **critical** | Blocks `execution.type: command` (shell injection) | | 3 | `no-ssrf` | **critical** | Blocks internal IPs/hostnames in URLs | | 4 | `no-unauthorized-credentials` | **high** | Blocks credentials not in `allowedCredentials` | | 5 | `reserved-namespace` | **high** | Blocks tool names starting with protected prefixes | | 6 | `force-approval` | **medium** | Enforces `requires_approval: true` | | 7 | `allowed-http-methods` | **high** | Blocks HTTP methods not in `allowedHttpMethods` | | 8 | `allowed-domains` | **high** | Blocks domains not in `allowedDomains` | | 9 | `force-draft-status` | **medium** | Enforces `status: 'draft'` on new tools | #### SSRF (Server Side Request Forgery) Blocked Patterns The `no-ssrf` rule blocks URLs targeting: - `169.254.169.254`: AWS/cloud metadata endpoint - `10.*`, `172.16-31.*`, `192.168.*`, RFC 1918 private networks - `localhost`, `127.0.0.1`, `0.0.0.0`, Loopback addresses - `*.internal`, `*.local`, Internal DNS suffixes - `metadata.google.internal`: GCP metadata > **This check also runs a second time, at execution.** The rule above only ever sees a tool's raw, > unresolved URL, any `{placeholder}` tokens are blanked out before checking, since the real value > isn't known yet at creation/approval time. A URL like `http://{host}/{path}` therefore passes this > creation-time check unconditionally. To close that gap, `HttpExecutor`/`http_executor.py` re-run the > same SSRF check against the **fully-resolved** URL immediately before the real HTTP request fires - > so a call that resolves `{host}` to `169.254.169.254` at execution time is still blocked with a > `POLICY_DENIED` error, even though the tool definition itself validated cleanly. ### Execution-time network and file safeguards These checks run when a tool executes, whatever its trust level, and are separate from the 9 content rules above. | Safeguard | What it does | Setting | |-----------|--------------|---------| | Resolved-address check | HTTP tools and the `web`, `web_scraper` and `extract_from_file` tools refuse a hostname that resolves to an internal address: loopback (all of `127.0.0.0/8`), private ranges, link-local, carrier-grade NAT (`100.64.0.0/10`), IPv4-mapped and NAT64 IPv6, and unique-local IPv6. TypeScript connects to the address it checked, so a DNS answer cannot change between the check and the connection | `MATIMO_ALLOW_PRIVATE_DNS=true` lets a tool reach an internal service by DNS name. Literal internal IPs, internal suffixes such as `*.internal` and redirects to them stay blocked | | Redirect re-check | Every redirect hop is validated again before the next request is made | none | | File roots | `read`, `edit`, `search`, `convert_to_file` and `extract_from_file` refuse a path whose real location, after resolving symlinks, is outside the listed directories | `MATIMO_FILE_ROOTS` is a list of directories separated by `:` (POSIX) or `;` (Windows). Unset means no confinement | | Function code location | A function tool's `execution.code` must stay inside the tool's own directory | none | A refused network or file call throws `POLICY_DENIED`. A function tool whose code lies outside its directory fails with an execution error. Both SDKs apply the same rules. ### Violation Severities | Severity | Meaning | Effect | |----------|---------|--------| | `critical` | Security vulnerability | Tool rejected, cannot be created or loaded | | `high` | Policy violation | Tool rejected, cannot be created or loaded | | `medium` | Best practice enforcement | Warning, tool created but flagged | | `low` | Informational | Advisory only | **Rejection threshold:** Any violation with severity `critical` or `high` causes the tool to be rejected. ### Using validateToolContent() ```typescript import { validateToolContent, validateToolDefinition } from 'matimo'; const tool = validateToolDefinition({ name: 'my_tool', version: '1.0.0', description: 'My tool', execution: { type: 'command', command: 'rm', args: ['-rf', '/'] }, }); const result = validateToolContent(tool, { source: 'untrusted' }); console.log(result.valid); // false console.log(result.violations); // [{ rule: 'no-command-execution', severity: 'critical', ... }] console.log(result.riskLevel); // 'high' ``` --- ## Risk Classification Risk is classified deterministically based on execution type and HTTP method: | Risk Level | Criteria | |-----------|---------| | **critical** | `execution.type: function` (arbitrary code) | | **high** | `execution.type: command` (shell), HTTP `DELETE`, or `requires_approval: true` | | **medium** | HTTP `POST`, `PUT`, `PATCH` | | **low** | HTTP `GET`, `HEAD`, `OPTIONS` | ```typescript import { classifyRisk } from 'matimo'; const risk = classifyRisk(toolDefinition); // Returns: 'low' | 'medium' | 'high' | 'critical' ``` Risk classification is deterministic, the same tool definition always produces the same risk level. --- ## Tool Lifecycle The full lifecycle for agent-created tools: ![Tool lifecycle in four steps: create a YAML draft, approve it with an HMAC signature, reload the registry, then use the tool.](../../../../assets/diagrams/tool-lifecycle-light.svg) ![Tool lifecycle in four steps: create a YAML draft, approve it with an HMAC signature, reload the registry, then use the tool.](../../../../assets/diagrams/tool-lifecycle-dark.svg) HMAC stands for Hash-based Message Authentication Code. ### Step 1: Create a Tool Use `matimo_create_tool` to write a new tool definition to disk. **Via SDK:** ```typescript const result = await matimo.execute('matimo_create_tool', { name: 'city_lookup', target_dir: './agent-tools', yaml_content: ` name: city_lookup version: '1.0.0' description: Look up user information including city and address details parameters: id: type: string required: true description: User ID to look up (1-10) execution: type: http method: GET url: 'https://jsonplaceholder.typicode.com/users/{id}' `, }); console.log(result); // { // success: true, // path: './agent-tools/city_lookup/definition.yaml', // riskLevel: 'low', // status: 'draft', ← forced by policy // message: 'Tool created as draft. Use matimo_approve_tool to promote.' // } ``` **What happens internally:** 1. **Name sanitization**: blocks path traversal (`../`), control characters, `matimo_` prefix 2. **YAML parsing**: validates syntax 3. **Safety fields forced**: `requires_approval: true` and `status: 'draft'` always set 4. **Schema validation**: validates against Zod ToolDefinition schema 5. **Content validation**: runs all 9 content rules 6. **Risk classification**: assigns risk level 7. **Write to disk**: creates `{target_dir}/{name}/definition.yaml` **What gets blocked:** ```typescript // Shell command tool, blocked by content validator await matimo.execute('matimo_create_tool', { name: 'file_reader', yaml_content: ` name: file_reader execution: type: command command: cat args: ['{path}'] `, }); // Error: Tool failed policy validation // [critical] no-command-execution: Command-type tools are not allowed // SSRF tool, blocked await matimo.execute('matimo_create_tool', { name: 'metadata_probe', yaml_content: ` execution: type: http url: 'http://169.254.169.254/latest/meta-data/' `, }); // Error: [critical] no-ssrf: URL targets internal/metadata endpoint // Namespace hijack, blocked await matimo.execute('matimo_create_tool', { name: 'matimo_backdoor', yaml_content: '...', }); // Error: Tool name cannot start with reserved namespace "matimo_" ``` ### Step 2: Approve a Tool Use `matimo_approve_tool` to promote a draft tool to approved status. ```typescript const result = await matimo.execute( 'matimo_approve_tool', { name: 'city_lookup', tool_dir: './agent-tools' }, { context: { agentId: 'reviewer', roles: ['admin'] } } // approving needs the admin role ); console.log(result); // { // success: true, // name: 'city_lookup', // hash: 'a1b2c3d4...', ← SHA-256 hash of YAML content // approvedAt: '2026-03-14T...', // message: 'Tool approved. Effective after reload.' // } ``` **What happens internally:** 1. **Read definition** from `{tool_dir}/{name}/definition.yaml` 2. **Re-validate**: runs content validator again (prevents approve-after-modify attacks) 3. **Compute hash**: SHA-256 of the YAML content 4. **HMAC sign**: creates cryptographic approval signature 5. **Update YAML**: changes `status: draft` → `status: approved` 6. **Write manifest**: saves to `.matimo-approvals.json` **HMAC Approval Manifest:** The approval is stored as a signed record: ```json { "city_lookup": { "hash": "sha256:a1b2c3d4...", "signature": "hmac-sha256:...", "approvedAt": "2026-03-14T09:30:00.000Z", "approvedBy": "system" } } ``` If someone modifies the YAML after approval, the hash won't match and the approval is automatically revoked on the next reload. ### Step 3: Reload Tools Use `matimo_reload_tools` to hot-reload all tools from disk into the live registry. ```typescript // Via meta-tool (works from SDK, LangChain, and MCP) const result = await matimo.execute('matimo_reload_tools', {}); console.log(result); // { // success: true, // loaded: 13, // removed: 0, // revalidated: 1, ← untrusted tools re-checked against policy // rejected: [], // message: 'Reload complete. 13 tools loaded, 0 removed, 0 rejected.' // } // Or programmatically (SDK only) const reloadResult = await matimo.reloadTools(); ``` **What happens internally:** 1. **Clear registry**: removes all tools from memory 2. **Re-read YAML** from all configured `toolPaths` 3. **Re-validate untrusted**: tools from `untrustedPaths` run through `canCreate()` policy check 4. **Reject violations**: tools with critical/high violations are rejected 5. **Register**: approved tools added to registry 6. **Track integrity**: SHA-256 hashes recorded for tamper detection 7. **Emit event**: `tools:reloaded` audit event with counts **Why is matimo_reload_tools a meta-tool?** Because it enables the full create→approve→reload→use lifecycle from **any** interface: | Interface | How to Reload | |-----------|--------------| | SDK | `matimo.reloadTools()` or `matimo.execute('matimo_reload_tools', {})` | | LangChain | Agent calls `matimo_reload_tools` tool | | MCP | Client calls `tools/call` with `name: 'matimo_reload_tools'` | Without this tool, MCP clients had no way to trigger a reload, they'd need SDK access. ### Step 4: Use the Tool After reload, the tool is in the registry and can be executed: ```typescript // The newly created tool is now available const tools = matimo.listTools(); console.log(tools.map(t => t.name)); // [..., 'city_lookup'] // Execute it const result = await matimo.execute('city_lookup', { query: 'London' }); ``` Note: Agent-created tools always have `requires_approval: true`, so every execution triggers an approval prompt (over MCP, an elicitation request to the client's user). ### Full Lifecycle Example ```typescript import { MatimoInstance, setGlobalMatimoInstance } from 'matimo'; import type { PolicyConfig } from 'matimo'; // 1. Configure const policyConfig: PolicyConfig = { allowedDomains: ['jsonplaceholder.typicode.com'], allowedHttpMethods: ['GET'], allowCommandTools: false, allowFunctionTools: false, }; // 2. Give the instance a reviewer: creating, approving, reloading and // calling the new tool all need approval. const matimo = await MatimoInstance.init({ toolPaths: ['./core-tools', './agent-tools'], untrustedPaths: ['./agent-tools'], policyConfig, onApproval: async (request) => { console.log(`Approve ${request.toolName}? [y/n]`); return true; // or prompt user }, }); setGlobalMatimoInstance(matimo); // meta-tools act on this instance // 3. Create await matimo.execute('matimo_create_tool', { name: 'city_lookup', target_dir: './agent-tools', yaml_content: ` name: city_lookup version: '1.0.0' description: Look up user information including city and address details parameters: id: { type: string, required: true } execution: type: http method: GET url: 'https://jsonplaceholder.typicode.com/users/{id}' `, }); // 4. Approve await matimo.execute( 'matimo_approve_tool', { name: 'city_lookup', tool_dir: './agent-tools' }, { context: { agentId: 'reviewer', roles: ['admin'] } } // approving needs the admin role ); // 5. Reload await matimo.execute('matimo_reload_tools', {}); // 6. Use const user = await matimo.execute('city_lookup', { id: '1' }); console.log(user); // { success: true, data: { name: "Leanne Graham", address: { city: "Gwenborough" } } } ``` --- ## Approval System ### How Approval Works ![Decision flow for a tool call: if the call needs approval it checks for a pre-approved pattern, otherwise asks the onApproval handler per call, per instance or process-wide, and either executes or throws a MatimoError.](../../../../assets/diagrams/approval-decision-light.svg) ![Decision flow for a tool call: if the call needs approval it checks for a pre-approved pattern, otherwise asks the onApproval handler per call, per instance or process-wide, and either executes or throws a MatimoError.](../../../../assets/diagrams/approval-decision-dark.svg) Full reference: [Approval System](https://matimo.dev/docs/api-reference/approval-system.md). ### Interactive Terminal Approval ```typescript import { MatimoInstance, type ApprovalRequest } from 'matimo'; import readline from 'readline'; async function askInTerminal(request: ApprovalRequest): Promise { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); console.log(`\nTool: ${request.toolName}`); console.log(`Description: ${request.description}`); console.log(`Params: ${JSON.stringify(request.params)}`); const answer = await new Promise((resolve) => rl.question('Approve? (y/n): ', resolve)); rl.close(); return answer.toLowerCase() === 'y'; } const matimo = await MatimoInstance.init({ autoDiscover: true, onApproval: askInTerminal }); ``` ### Tests and CI Prefer an `onApproval` that encodes what the test allows, or `MATIMO_APPROVED_PATTERNS` listing the tools it may run. `MATIMO_AUTO_APPROVE=true` approves everything unseen (except `matimo_approve_tool`) and logs a warning; keep it to throwaway environments. ### Pre-Approved Patterns ```bash # Approve specific tools or patterns export MATIMO_APPROVED_PATTERNS="calculator,weather_*,search" # Supports wildcards: # calculator → exact match # weather_* → matches weather_get, weather_forecast, etc. # * → matches everything except matimo_approve_tool ``` ### Session Whitelisting In interactive mode, approved tools can be added to a session whitelist so subsequent calls skip the prompt: ```typescript const whitelist = new Set(); const matimo = await MatimoInstance.init({ autoDiscover: true, onApproval: async (request) => { // Skip prompt if already approved this session if (whitelist.has(request.toolName)) { return true; } const approved = await promptUser(request); if (approved) { whitelist.add(request.toolName); } return approved; }, }); ``` ### MCP Approval Flow When tools are called via MCP there is no terminal to prompt, so the server asks the human behind the MCP client with an [elicitation](https://modelcontextprotocol.io/specification/draft/client/elicitation) request, a yes/no form the client shows its user: ![An MCP tools/call runs matimo.execute with an onApproval handler. If the call needs approval and the client supports elicitation, the user is asked; accepting runs the tool, anything else rejects it. Clients without elicitation get an error.](../../../../assets/diagrams/mcp-elicitation-light.svg) ![An MCP tools/call runs matimo.execute with an onApproval handler. If the call needs approval and the client supports elicitation, the user is asked; accepting runs the tool, anything else rejects it. Clients without elicitation get an error.](../../../../assets/diagrams/mcp-elicitation-dark.svg) The model is never told how to approve its own call. `_matimo_approved` exists only for clients that confirm every call with their user themselves: start the server with `trustClientApproval: true` (`trust_client_approval=True` in Python) and approval-requiring tools gain an optional `_matimo_approved` boolean, which then counts as approval. Without that option the parameter is not advertised and is ignored. Pre-approved tools (`MATIMO_APPROVED_PATTERNS`) run without asking either way. MCP calls run with no roles unless the operator grants some with the server's `context` option (`MCPServerOptions.context` in both SDKs), e.g. `{ agentId: 'claude-desktop', roles: ['admin'] }` for a single-user local server. Role-gated tools refuse a context without their role, e.g. `matimo_approve_tool` requires `admin` whenever a context is supplied. The model cannot set this context: it comes from the server's options, not from the tool arguments. **Beyond policy gating**, each MCP tool registration also carries the protocol's standard `readOnlyHint`/`destructiveHint`/`idempotentHint`/`openWorldHint` annotations, derived directly from `execution.type`/HTTP method rather than from the aggregate risk tier above (the two signals can diverge, a GET and a DELETE tool can share a risk tier while having opposite hints). A denied or failed call returns `isError: true` with a `structuredContent` field (`code`/`statusCode`/`retryable`/`message`) instead of only a text string, and every successful result passes through the response-size guardrail before being returned. See [MCP Server docs, Tool Metadata & Error Responses](https://matimo.dev/docs/mcp-server.md#tool-metadata--error-responses). ## HITL Quarantine Traditional policy engines are binary, a tool is either allowed or blocked. HITL (human-in-the-loop) quarantine adds a third state, **pending_approval**: with `enableHITL` on, a call at or above `hitlMinRiskLevel` pauses until a person approves it. HITL quarantine is separate from per-call approval (`onApproval`, see [APPROVAL-SYSTEM.md](https://matimo.dev/docs/api-reference/approval-system.md)). Quarantine is decided by the policy engine from the tool's risk; approval by the tool's `requires_approval`, DELETE/command type, or destructive keywords. When both apply, quarantine is resolved first. ### How Quarantine Works ![policy.canExecute returns allowed, pending approval or false. Allowed executes, false throws POLICY_DENIED, and pending approval goes to the approval manifest or onHITL callback, which either executes or throws POLICY_DENIED.](../../../../assets/diagrams/can-execute-flow-light.svg) ![policy.canExecute returns allowed, pending approval or false. Allowed executes, false throws POLICY_DENIED, and pending approval goes to the approval manifest or onHITL callback, which either executes or throws POLICY_DENIED.](../../../../assets/diagrams/can-execute-flow-dark.svg) ### Enabling HITL Pass `enableHITL: true` in your policy config and provide an `onHITL` callback: ```typescript import { MatimoInstance } from '@matimo/core'; import type { HITLRequest } from '@matimo/core'; const matimo = await MatimoInstance.init({ toolPaths: ['./tools', './agent-tools'], untrustedPaths: ['./agent-tools'], policyConfig: { allowedDomains: ['api.example.com'], enableHITL: true, hitlMinRiskLevel: 'medium', // pause every POST/PUT/PATCH, DELETE and other high-risk call }, onHITL: async (request: HITLRequest) => { console.log(`Tool: ${request.toolName}`); console.log(`Risk: ${request.riskLevel}`); console.log(`Reason: ${request.reason}`); // Wire this to Slack, email, a UI, or a terminal prompt return await askHumanOperator(request); }, }); ``` ```python matimo = await Matimo.init( "./tools", untrusted_paths=["./agent-tools"], policy_config=PolicyConfig(enable_hitl=True, hitl_min_risk_level="medium"), on_hitl=ask_human_operator, ) ``` ### HITLCallback & HITLRequest ```typescript /** Called when a call is quarantined. Return true to approve, false to reject. */ type HITLCallback = (request: HITLRequest) => Promise; interface HITLRequest { toolName: string; riskLevel: RiskLevel; // 'low' | 'medium' | 'high' | 'critical' reason: string; // Why the call was quarantined environment?: string; // From the caller's PolicyContext agentId?: string; // Calling agent identifier toolDefinition?: unknown; // Full tool definition for admin review } ``` ### Resolution Flow When a call is `pending_approval`, Matimo resolves it in order: 1. **Check the approval manifest**: if this tool definition was approved before and its YAML hash hasn't changed, the call runs 2. **Invoke the HITL callback**: call `onHITL(request)` and wait for the answer 3. **Fail closed**: with no callback, the call is rejected An approval is recorded in the signed approval manifest against the definition's hash, so the reviewer is asked once per tool definition, and again if the YAML changes. ### Quarantine Events | Event Type | When Emitted | |-----------|-------------| | `tool:quarantined` | A quarantined call is about to be sent to `onHITL`, or an untrusted tool is held as pending when it loads | | `tool:quarantine_approved` | A quarantined call was approved (manifest or callback) | | `tool:quarantine_rejected` | A quarantined call was refused, or nobody could approve it | ```typescript const matimo = await MatimoInstance.init({ // ... onEvent: (event) => { if (event.type === 'tool:quarantine_approved') { console.log(`${event.toolName} approved`); } if (event.type === 'tool:quarantine_rejected') { console.log(`${event.toolName} rejected`); } }, }); ``` ### Quarantine Risk Levels The two settings act at different times. **When a call runs**, `canExecute()` quarantines it when its **execution risk** is at or above `hitlMinRiskLevel` (default: the least severe entry of `quarantineRiskLevels`, so `'medium'` out of the box). A policy that reviews POSTs therefore also reviews DELETEs. Execution risk is `classifyRisk()` with one difference: a `type: function` tool uses its declared `risk:` (default `high`, raised to `high` by `requires_approval: true`) instead of always rating `critical`. Function tools in the registry are always developer-authored, agent-created function tools are rejected at creation, so `calculator` (`risk: low`) is not quarantined while `execute` (`risk: critical`) is. | Tool | Execution risk | `enableHITL: true` (defaults) | |------|----------------|-------------------------------| | HTTP GET | low | runs | | HTTP POST / PUT / PATCH | medium | quarantined | | HTTP DELETE, `requires_approval: true`, `type: command` | high | quarantined | | `type: function` with `risk: low` | low | runs | | `type: function` with no `risk:` | high | quarantined | The full matrix both SDKs are tested against is `conformance/policy/execution-quarantine.json`. **When an untrusted tool loads** (`untrustedPaths`, or a reload after `matimo_create_tool`), `canCreate()` uses `quarantineRiskLevels` as a list: a tool whose most severe content violation, or (in production) whose risk, is *listed* is held as pending instead of rejected. Agent-created tools carry `requires_approval: true`, which `classifyRisk()` rates `high`, so with the default `['medium']` they are rejected in production rather than held. Use `quarantineRiskLevels: ['medium', 'high']` if you want them held for review there. You can change the HITL callback at runtime (TypeScript): ```typescript matimo.setHITLCallback(async (request) => request.riskLevel === 'medium'); matimo.setHITLCallback(null); // back to fail-closed ``` Python sets `on_hitl` at `init()` only. --- ## Policy Hot-Reload TypeScript can swap the policy at runtime without restarting the process; all tools are re-validated against the new policy. (Python has no `reload_policy()` yet; create a new `Matimo` instance.) ### reloadPolicy() ```typescript // Option 1: Reload from a new YAML file await matimo.reloadPolicy('./policy-prod.yaml'); // Option 2: Reload with an inline PolicyConfig await matimo.reloadPolicy({ allowedDomains: ['api.production.example.com'], enableHITL: true, hitlMinRiskLevel: 'medium', }); // Option 3: Re-read the original policyFile (if MatimoInstance was initialized with one) await matimo.reloadPolicy(); ``` **What happens internally:** 1. The new policy is validated (Zod schema for YAML files) 2. The old policy is replaced and the new one frozen 3. A `policy:reloaded` event is emitted 4. `reloadTools()` runs, re-validating every tool against the new policy 5. Tools that no longer pass are left out of the registry **Return value:** the `ReloadResult` of that re-validation: ```typescript const result = await matimo.reloadPolicy('./policy-strict.yaml'); console.log(result.loaded); // Tools registered console.log(result.rejected); // Tools the new policy rejected ``` ### parsePolicyFile() Parse a YAML policy file without applying it, useful for validation: ```typescript import { parsePolicyFile } from '@matimo/core'; const config = parsePolicyFile('./policy.yaml'); // Returns a validated PolicyConfig; throws if the YAML or schema is invalid ``` ### Hot-Reload Events ```typescript const matimo = await MatimoInstance.init({ onEvent: (event) => { if (event.type === 'policy:reloaded') { console.log('Policy reloaded at', event.timestamp); } }, }); ``` **Use case: reload when the file changes:** ```typescript import fs from 'fs'; fs.watch('./policy.yaml', async () => { try { const result = await matimo.reloadPolicy('./policy.yaml'); console.log(`Policy reloaded: ${result.loaded} tools, ${result.rejected.length} rejected`); } catch (err) { console.error('Policy reload failed, keeping previous policy:', (err as Error).message); } }); ``` --- ## Integrity & Tamper Detection ### SHA-256 Integrity Tracking `ToolIntegrityTracker` hashes tool definitions so a reload can tell new, unchanged and modified tools apart: ```typescript import { ToolIntegrityTracker } from '@matimo/core'; const tracker = new ToolIntegrityTracker(); // After a tool is validated and loaded tracker.record('my_tool', yamlContent, 'untrusted'); // On the next load const { action, reason } = tracker.onToolLoaded('my_tool', yamlContent, 'untrusted'); ``` | `action` | `reason` | Meaning | |----------|----------|---------| | `validate` | `new-tool` | Never seen before; validate it | | `keep` | `unchanged` | Same hash and source; safe to skip | | `revalidate` | `content-modified` / `source-changed` | The YAML changed, or it moved between trusted and untrusted paths | ### HMAC Approval Manifest `ApprovalManifest` stores signed approvals in `/.matimo-approvals.json`: ```typescript import { ApprovalManifest } from '@matimo/core'; const manifest = new ApprovalManifest('./', process.env.MATIMO_APPROVAL_SECRET); const hash = manifest.computeHash(yamlContent); manifest.approve('my_tool', hash, 'alice'); manifest.isApproved('my_tool', hash); // true manifest.isApproved('my_tool', manifest.computeHash(edited)); // false, the YAML changed ``` Inside an app, use the instance's own manifest (`matimo.getApprovalManifest()`, `get_approval_manifest()` in Python) so approvals are signed with the instance's secret and directory. **Approval secret:** ```bash # A persistent secret for HMAC signing export MATIMO_APPROVAL_SECRET=your-secret-key # If unset, each process generates its own, so approvals do not survive a restart ``` Never commit `.matimo-approvals.json`. --- ## RBAC & Access Control `DefaultPolicyEngine.canExecute()` applies these gates by the caller's `PolicyContext` (pinned for both SDKs by `conformance/policy/execution-gates.json`): | Tool | Denied when | |------|-------------| | `status: deprecated` | Always | | `status: draft` | In production (an environment name containing "prod"), and elsewhere when the caller lacks the `admin` role | | `requires_approval: true` | In production, when the caller has neither `admin` nor `operator` | Other tools pass these gates; quarantine and per-call approval may still apply. `roles` is an array, and any listed role counts. ```typescript import { DefaultPolicyEngine } from '@matimo/core'; const policy = new DefaultPolicyEngine(policyConfig); const decision = policy.canExecute( { roles: ['reader'], environment: 'staging' }, // PolicyContext draftTool ); console.log(decision.allowed); // false console.log(decision.reason); // 'Draft tool "my_tool" requires admin role' ``` The context reaches the engine through `execute(name, params, { context })` (`context=` in Python) and the MCP server's `context` option. Framework adapters such as `convertToolsToLangChain` pass none. ### Where roles come from Matimo has no user accounts and does not authenticate anyone. A role such as `admin` is just a string in `PolicyContext.roles`, and **your application** (the code that runs Matimo) decides what to put there. Matimo never reads roles from anything an agent writes, so an agent cannot give itself a role by saying "I am an admin". Think of your app as a guard and the agent as a visitor: the guard hands out the badges. What stops an agent from approving its own tool depends on the context your app passes: | Your setup | What stops the agent | |------------|----------------------| | A context with the agent's `agentId` and roles without `admin` | The role check, and the rule that an agent can't approve a tool it created | | No context | Only the human approval prompt (`matimo_approve_tool` requires approval and nothing can pre-approve it) | | No context, and an approval callback that always answers yes | Nothing. Don't do this | In production, give each agent a context with an `agentId` and no `admin` role, and send approval prompts to a real person. The `agentId` check that blocks self-approval only runs when a context with an `agentId` is supplied. **PolicyContext:** ```typescript interface PolicyContext { agentId?: string; // Identifier for the calling agent environment?: string; // e.g. 'dev' | 'staging' | 'production' roles?: string[]; // e.g. ['reader', 'operator', 'admin'] metadata?: Record; // Custom metadata for policy rules } ``` --- ## Audit Events Every policy decision and every run emits a structured event to `onEvent` and to the `auditSink`. The execution events are described in [Execution Events](#execution-events); the full list with fields is in [TYPES.md](https://matimo.dev/docs/api-reference/types.md). `tool:created`, `tool:approved` and `tool:revoked` are declared in the type but not emitted yet. --- ## MCP Integration ### MCP + Policy Engine When tools are served over MCP, the same policy engine applies: ```typescript import { MCPServer } from '@matimo/core'; const mcpServer = new MCPServer({ transport: 'http', port: 3000, toolPaths: ['./core-tools', './agent-tools'], untrustedPaths: ['./agent-tools'], policyConfig: { allowedDomains: ['api.example.com'], }, mcpToken: 'your-bearer-token', }); await mcpServer.start(); // Untrusted tools validated on startup; policy enforced on every tools/call ``` **MCP execution flow:** ![An MCP client POSTs tools/call to the MCPServer handler, which calls matimo.execute with an onApproval elicitation handler. Execute runs the policy check, approval, credential injection and executor routing, and the result returns as an MCP response.](../../../../assets/diagrams/mcp-http-flow-light.svg) ![An MCP client POSTs tools/call to the MCPServer handler, which calls matimo.execute with an onApproval elicitation handler. Execute runs the policy check, approval, credential injection and executor routing, and the result returns as an MCP response.](../../../../assets/diagrams/mcp-http-flow-dark.svg) Each MCP tool registration also carries the protocol's standard `readOnlyHint`/`destructiveHint`/`idempotentHint`/`openWorldHint` annotations, derived from `execution.type` and the HTTP method rather than from the risk tier (the two can diverge). A denied or failed call returns `isError: true` with a `structuredContent` field (`code`/`statusCode`/`retryable`/`message`), and every successful result passes through the response-size guardrail. See [MCP Server docs, Tool Metadata & Error Responses](https://matimo.dev/docs/mcp-server.md#tool-metadata--error-responses). ### MCP + Tool Lifecycle The complete create→approve→reload→use lifecycle works via MCP. `curl` cannot answer elicitation requests, so this walkthrough assumes a server started with `trustClientApproval: true`, where `_matimo_approved: true` counts as the approval. With an elicitation-capable client, leave the flag out and answer the prompts instead. ```bash # 1. Create a tool via MCP curl -X POST http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -H "Mcp-Session-Id: $SESSION" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "matimo_create_tool", "arguments": { "name": "my_new_tool", "target_dir": "./agent-tools", "yaml_content": "name: my_new_tool\nversion: '\''1.0.0'\''\n...", "_matimo_approved": true } } }' # 2. Approve it curl -X POST http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -H "Mcp-Session-Id: $SESSION" \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "matimo_approve_tool", "arguments": { "name": "my_new_tool", "tool_dir": "./agent-tools", "_matimo_approved": true } } }' # 3. Reload (brings new tool into registry + notifies MCP clients) curl -X POST http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -H "Mcp-Session-Id: $SESSION" \ -d '{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "matimo_reload_tools", "arguments": { "_matimo_approved": true } } }' # 4. Use the new tool (now in tools/list) curl -X POST http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -H "Mcp-Session-Id: $SESSION" \ -d '{ "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "my_new_tool", "arguments": { "query": "test", "_matimo_approved": true } } }' ``` --- ## LangChain Agent Integration ### Setup ```typescript import { MatimoInstance, convertToolsToLangChain, setGlobalMatimoInstance } from 'matimo'; import { ChatOpenAI } from '@langchain/openai'; import type { ToolDefinition, PolicyConfig } from 'matimo'; const matimo = await MatimoInstance.init({ toolPaths: ['./tools', './agent-tools'], untrustedPaths: ['./agent-tools'], policyConfig: { /* ... */ }, // Human-in-the-loop approval for every call that needs it onApproval: async (request) => { console.log(`Agent wants to call: ${request.toolName}`); return true; // or prompt user }, }); setGlobalMatimoInstance(matimo); // meta-tools act on this instance // Convert Matimo tools to LangChain format const tools = matimo.listTools(); const langchainTools = await convertToolsToLangChain(tools as ToolDefinition[], matimo); // Create LLM with tools bound const llm = new ChatOpenAI({ model: 'gpt-4o-mini', temperature: 0 }); let llmWithTools = llm.bindTools(langchainTools); ``` ### Full Lifecycle from LangChain Agent ```typescript // Agent creates a tool → human approves → reload → agent uses it // 1. Agent calls matimo_create_tool (LLM decides this autonomously) const createResult = await matimo.execute('matimo_create_tool', { name: 'city_lookup', target_dir: './agent-tools', yaml_content: '...', }); // 2. A reviewer approves it (the agent cannot approve its own tool) await matimo.execute( 'matimo_approve_tool', { name: 'city_lookup', tool_dir: './agent-tools' }, { context: { agentId: 'reviewer', roles: ['admin'] } } // approving needs the admin role ); // 3. Agent calls matimo_reload_tools await matimo.execute('matimo_reload_tools', {}); // 4. IMPORTANT: Rebind LangChain tools (registry changed) const updatedTools = matimo.listTools(); const updatedLangchainTools = await convertToolsToLangChain( updatedTools as ToolDefinition[], matimo ); llmWithTools = llm.bindTools(updatedLangchainTools); // 5. Now the agent can call the new tool const result = await matimo.execute('city_lookup', { id: '1' }); ``` > **Important:** After `matimo_reload_tools`, you must rebind LangChain tools because the registry has changed. The LLM needs an updated tool list to know about newly available tools. --- ## API Reference ### MatimoInstance | Method | Returns | Description | |--------|---------|-------------| | `MatimoInstance.init(config)` | `Promise` | Initialize with policy and tools | | `matimo.execute(name, params)` | `Promise` | Execute a tool (policy enforced) | | `matimo.listTools(context?)` | `ToolDefinition[]` | List available tools (policy filtered) | | `matimo.searchTools(query)` | `ToolDefinition[]` | Search tools by name/description | | `matimo.reloadTools()` | `Promise` | Hot-reload tools from disk | | `matimo.reloadSkills()` | `Promise<{ loaded: number; removed: number }>` | Hot-reload skills from configured `skillPaths` | | `matimo.hasPolicy()` | `boolean` | Always `true`, `init()` always constructs a `DefaultPolicyEngine()` when no `policy`/`policyFile`/`policyConfig` is given, so every instance is policy-gated by default | | `matimo.reloadPolicy(configOrFile?)` | `Promise` | Hot-reload policy engine + re-validate tools | | `matimo.setHITLCallback(callback)` | `void` | Set or clear the HITL quarantine callback | | `matimo.setApprovalCallback(callback)` | `void` | Set or clear the instance's approval callback (`null` falls back to the process-wide handler) | | `matimo.getGovernanceMode()` | `'secure' \| 'legacy'` | The governance mode in effect | | `matimo.getApprovalManifest()` | `ApprovalManifest \| null` | The manifest `matimo_approve_tool` records approvals in | ### ReloadResult ```typescript interface ReloadResult { loaded: number; // Tools registered by this reload removed: number; // Tools no longer loaded revalidated: number; // Untrusted tools re-checked rejected: string[]; // Tool names that failed policy } ``` ### Policy Exports ```typescript import { // Policy engine DefaultPolicyEngine, validateToolContent, isSSRFTarget, classifyRisk, loadPolicyFromFile, parsePolicyFile, // Integrity ToolIntegrityTracker, ApprovalManifest, // Approval ApprovalHandler, getGlobalApprovalHandler, // Types type PolicyEngine, type PolicyConfig, type PolicyContext, type PolicyDecision, type RiskLevel, type Violation, type ValidationResult, type ValidationContext, type MatimoEvent, type MatimoEventHandler, type ReloadResult, type ApprovalRequest, type ApprovalCallback, type HITLCallback, type HITLRequest, } from 'matimo'; ``` --- ## Examples ### Policy Demo (Full 11-Mission Autonomous Agent) ```bash cd examples/tools export OPENAI_API_KEY=sk-... printf "y\ny\ny\ny\nn\ny\n" | pnpm policy:demo ``` See [examples/tools/policy/README.md](https://github.com/tallclub/matimo/blob/main/typescript/examples/tools/policy/README.md) for detailed documentation. ### Minimal Policy Setup ```typescript const matimo = await MatimoInstance.init({ toolPaths: ['./tools'], policyConfig: { allowedDomains: ['api.example.com'], allowCommandTools: false, }, }); ``` ### Interactive Approval with Whitelist ```typescript const whitelist = new Set(); const matimo = await MatimoInstance.init({ toolPaths: ['./tools'], onApproval: async (req) => { if (whitelist.has(req.toolName)) return true; const approved = await askUser(`Approve ${req.toolName}?`); if (approved) whitelist.add(req.toolName); return approved; }, }); ``` ### MCP Server with Policy ```typescript const server = new MCPServer({ transport: 'http', port: 3000, policyConfig: { allowedDomains: ['api.github.com'], allowCommandTools: false, }, mcpToken: process.env.MCP_TOKEN, }); await server.start(); ``` --- ## Python SDK: Policy & Lifecycle The Python SDK exposes the same policy engine, risk classifier, and lifecycle controls as TypeScript. The API mirrors the TypeScript version with Pythonic naming (snake_case). ### Quick Start (Python) ```python from matimo import Matimo, PolicyConfig # Load policy from file matimo = await Matimo.init( ['./tools', './agent-tools'], policy_file='./policy.yaml', untrusted_paths=['./agent-tools'], ) # Inline policy config matimo = await Matimo.init( './tools', policy_config=PolicyConfig( allowed_domains=['api.github.com', 'api.slack.com'], allowed_http_methods=['GET', 'POST'], allow_command_tools=False, allow_function_tools=False, protected_namespaces=['matimo_'], ), ) ``` `Matimo.init()` takes keyword arguments after the tool paths. ### Approval and HITL Callbacks ```python from matimo import ApprovalRequest, HITLRequest, Matimo, PolicyConfig # Per-call approval (requires_approval, DELETE/command tools, destructive keywords) async def on_approval(request: ApprovalRequest) -> bool: return input(f"Approve {request.tool_name} {request.params}? (y/n) ") == 'y' # Risk-based quarantine (enable_hitl): every call at or above hitl_min_risk_level async def on_hitl(request: HITLRequest) -> bool: return input(f"Run {request.tool_name} ({request.risk_level})? (y/n) ") == 'y' matimo = await Matimo.init( './tools', policy_config=PolicyConfig(enable_hitl=True, hitl_min_risk_level='high'), on_approval=on_approval, on_hitl=on_hitl, ) ``` Both callbacks return `True` to allow the call and `False` to refuse it. ### Policy Events (Python) ```python def on_event(event: dict) -> None: print(f"[{event['type']}] {event.get('tool_name')}") matimo = await Matimo.init('./tools', policy_file='./policy.yaml', on_event=on_event) ``` Events are dicts with snake_case keys; see [Execution Events](#execution-events) for `tool:executed` / `tool:execution_failed`. ### Risk Classification (Python) ```python from matimo import classify_execution_risk, classify_risk tool = matimo.get_tool('my_api_tool') print(classify_risk(tool).value) # risk as an untrusted proposal print(classify_execution_risk(tool).value) # risk of running it: 'low' | 'medium' | 'high' | 'critical' ``` ### Custom PolicyEngine (Python) ```python from matimo import Matimo, PolicyAllowed, PolicyDecision, PolicyDenied class MyPolicy: """Any object with these three methods satisfies the PolicyEngine protocol.""" def can_create(self, context, tool_def) -> PolicyDecision: return PolicyAllowed() def can_execute(self, context, tool) -> PolicyDecision: if tool.name.startswith('delete_'): return PolicyDenied(reason='Delete operations are not allowed for agents') return PolicyAllowed() def filter_for_agent(self, context, tools): return [t for t in tools if not t.name.startswith('delete_')] matimo = await Matimo.init('./tools', policy=MyPolicy()) ``` Return `PolicyPendingApproval(reason=..., risk_level=...)` from `can_execute` to quarantine a call for `on_hitl`. ### Tool Lifecycle (Python) ```python # 1. Validate result = await matimo.execute('matimo_validate_tool', {'yaml_content': yaml_str}) # 2. Create (asks on_approval: matimo_create_tool requires approval) result = await matimo.execute('matimo_create_tool', { 'name': 'my_tool', 'yaml_content': yaml_str, 'target_dir': './agent-tools', 'proposed_by': 'agent', 'justification': 'User requested this capability', }) # 3. Approve result = await matimo.execute('matimo_approve_tool', {'name': 'my_tool', 'tool_dir': './agent-tools'}, context=PolicyContext(agent_id='reviewer', roles=['admin'])) # 4. Reload result = await matimo.execute('matimo_reload_tools', {}) # 5. Use result = await matimo.execute('my_tool', {'query': 'hello'}) ``` > See [`python/examples/native/policy/policy_demo.py`](https://github.com/tallclub/matimo/blob/main/python/examples/native/policy/policy_demo.py) for a complete 11-mission Python policy demo. > Run with: `cd python && make policy-demo` ## Upgrading to 0.2.0 0.2.0 (TypeScript and Python) makes the governance described in this guide the default. Most of the changes fix places where a tool call wasn't governed the way these docs said. Those fixes apply to everyone. One new default, approval for DELETE and shell tools, can be switched off with `governanceMode: 'legacy'`. ### Quick path 1. Upgrade, then run your test suite or agent once. 2. If a DELETE or command tool now fails with `requires approval`, decide per tool: - Wire a reviewer: `onApproval` / `on_approval` at init (see [Approval System](#approval-system)). - Pre-approve it: `MATIMO_APPROVED_PATTERNS=my_delete_tool`. - Opt the tool out in its YAML: `requires_approval: false`. - Or restore the old default everywhere: `governanceMode: 'legacy'` in `InitOptions` or `policy.yaml` (`governance_mode="legacy"` in Python). 3. If you run `validate-tools` on your own tools, add `requires_approval` to each DELETE tool and `risk` to each function tool (see below). ### New default: DELETE and command tools ask before every call A tool whose YAML doesn't set `requires_approval` now needs per-call approval when it is an HTTP `DELETE` or a `type: command` tool. An explicit `requires_approval` always wins. The destructive keyword scan of `command` / `sql` parameters is unchanged. `governanceMode: 'legacy'` restores the old default, and nothing else. Both SDKs assert the same table, [`conformance/approval/definition-requires-approval.json`](https://github.com/tallclub/matimo/blob/main/conformance/approval/definition-requires-approval.json). ### Fixes that change behaviour (both modes) | Area | Before 0.2.0 | 0.2.0 | |---|---|---| | HITL quarantine at execution | Quarantined a tool only when its risk was *listed* in `quarantineRiskLevels`, so with the default `['medium']` a high-risk DELETE ran unquarantined | Quarantines every tool whose risk is **at or above** `hitlMinRiskLevel` (default: least severe entry of `quarantineRiskLevels`) | | Function tool risk at execution | Always `critical` | The tool's declared `risk:`, or `high` if it declares none. `requires_approval: true` raises it to at least `high` | | HITL approvals for tools loaded at init | TypeScript wrote the approval but never read it back. Python never used the manifest. Either way the reviewer was asked again on every call | Read from and recorded in the signed approval manifest, in both SDKs | | Python `requires_approval` | Ignored; only HITL quarantine ran | Enforced per call, as in TypeScript | | Python `execute(..., approved=True)` | Skipped policy denials too | Skips only the approval prompt; policy denials still apply | | Approval callback scope | One process-wide callback | Per call (`execute(..., { onApproval })`), then per instance (`InitOptions.onApproval`), then the global handler. With none, the call fails closed | | MCP approval | The error told the model to retry with `_matimo_approved: true`, and the server trusted it | The server asks the human through MCP elicitation. `_matimo_approved` is offered and honoured only with `trustClientApproval` / `trust_client_approval` | | MCP HTTP bearer check | String comparison | Constant-time comparison | | `matimo_approve_tool` | Any caller could approve any tool, including one it had just created; `MATIMO_AUTO_APPROVE` and patterns applied | Refuses when the caller's `agentId` created the tool. Requires the `admin` role when a policy context is supplied. Never pre-approved | | `MATIMO_AUTO_APPROVE=true` | Silent, and recommended in the approval error hint | Logs a warning at init. The hint recommends `onApproval` or `MATIMO_APPROVED_PATTERNS` | | `matimo_approve_tool` + reload | Without `MATIMO_APPROVAL_SECRET` (or with a different `approvalDir`) the tool signed with its own key, so the next reload rejected the tool it had just approved | Approvals are recorded with the owning instance and survive the reload | | `matimo_create_tool` / `matimo_get_tool_status` | Called a low-risk draft "auto-approved … ready for use" | Report `pending` until a human approves the draft | | `matimo_validate_tool` | TypeScript flagged the `requires_approval`/`status` fields creation sets itself; Python said `valid: true` despite critical violations | `valid` is true exactly when `matimo_create_tool` would accept the definition | | Python draft tools | Ran for anyone outside production, and for admins in production | Never in production; elsewhere only for the `admin` role, as in TypeScript | | Python untrusted tools | High-severity violations (disallowed domain, method or credential) rejected only in production; a tool with no `status` was always rejected | Critical and high violations reject in every environment; an unset `status` is accepted, as in TypeScript | | Python `no-ssrf` | Missed `*.internal`, `*.local`, `*.localhost` and `0.0.0.0` | Blocks them, as in TypeScript | | Python `ReloadResult` | `loaded` counted new names; `revalidated` counted replaced tools | `loaded` counts every registered tool; `revalidated` the untrusted tools re-checked, as in TypeScript | | Python LangChain tools | A failed call raised; LangGraph re-raises it, so a refused approval ended the agent run | The tool returns `"Error: "` to the model, as in TypeScript | | Python credential lookup | Read `MATIMO__` and ``, not the documented `MATIMO_` | Reads `MATIMO_` first (also from `credentials`); the older names still work | ### Events - TypeScript now emits `tool:executed` (it never did before) and the new `tool:execution_failed`. - Python renamed `duration` (seconds) to `duration_ms` (milliseconds). `trace_id` is a full UUID, no longer 8 characters. Python also emits the new `tool:execution_failed`. - The fields are the same in both SDKs; see [Execution Events](#execution-events). ### Tool authoring (validators) `pnpm validate-tools` and `make validate-tools` now reject: - An HTTP `DELETE` tool without an explicit `requires_approval` (set `true`, or `false` to opt out). - A `type: function` tool without a `risk:` level. ### New, additive - `auditSink` / `audit_sink` with `JsonlFileSink`, a hash-chained audit log ([Audit Sink](#audit-sink)). - `governanceMode` / `governance_mode` and `getGovernanceMode()` / `get_governance_mode()`. - `hitlMinRiskLevel` / `hitl_min_risk_level` in `PolicyConfig` and `policy.yaml`. - Function tools receive the caller's policy context: `(params, { credentials, policyContext })` in TypeScript, `run(params, context)` in Python. - The MCP server's `context` option sets the policy context for every MCP call. --- ## See Also - [Meta-Tools Reference](https://matimo.dev/docs/api-reference/meta-tools.md): Built-in tool lifecycle management tools - [Approval System](https://matimo.dev/docs/api-reference/approval-system.md): Complete approval handler configuration - [SDK Reference](https://matimo.dev/docs/api-reference/sdk.md): `Matimo.init()` InitOptions (Python) and `MatimoInstance.init()` (TypeScript) --- # Approval System Source: https://matimo.dev/docs/api-reference/approval-system/ Some tool calls should not run until a person says yes: deleting data, running a shell command, approving a tool an agent wrote. Matimo's approval system decides **which calls need a human**, and **who answers**. It works the same way in the TypeScript and Python SDKs and for every provider. > **Approval vs. HITL quarantine.** This page covers per-call approval (`onApproval`). The policy engine's separate, risk-based checkpoint, `enableHITL` + `hitlMinRiskLevel` + `onHITL`, is described in [Policy and Lifecycle](https://matimo.dev/docs/api-reference/policy-and-lifecycle.md). When both apply, the HITL check runs first, then approval. ## When a call needs approval A call needs approval when any of these is true: | Trigger | Example | |---------|---------| | The tool's YAML says `requires_approval: true` | `github-delete-repository`, `execute`, `matimo_create_tool` | | The YAML doesn't set `requires_approval`, and the tool is an HTTP `DELETE` or a `type: command` tool (secure mode, the 0.2.0 default) | a `delete_post` HTTP tool you define (no built-in tool is a command tool) | | A destructive keyword appears in the call's `sql` argument, or in `command` for a command tool | `{ sql: 'DROP TABLE users' }` | `requires_approval: false` opts a tool out of the first two triggers. It does **not** turn off the keyword scan: a call whose `sql` contains `DELETE` still asks. Check a definition without running it: ```typescript import { definitionRequiresApproval } from '@matimo/core'; definitionRequiresApproval(tool, 'secure'); // true for DELETE/command tools without requires_approval definitionRequiresApproval(tool, 'legacy'); // only true when the YAML says requires_approval: true ``` ```python from matimo import definition_requires_approval definition_requires_approval(tool, "secure") ``` `governanceMode: 'legacy'` (`governance_mode="legacy"`, or `governanceMode: legacy` in `policy.yaml`) restores the pre-0.2.0 default, where DELETE and command tools ask only if their YAML says so. See the [migration guide](https://matimo.dev/docs/api-reference/policy-and-lifecycle.md). ## Who answers When a call needs approval, Matimo checks these in order and uses the first that applies: 1. **Pre-approval**: the tool's name matches a pattern in `MATIMO_APPROVED_PATTERNS` (or `MATIMO_AUTO_APPROVE=true`). The call runs without asking. 2. **`onApproval` passed to `execute()`** for this one call. 3. **`onApproval` passed to `MatimoInstance.init()`** (or set later with `setApprovalCallback`). 4. **The process-wide handler's callback**: `getGlobalApprovalHandler().setApprovalCallback()`, kept for older code. 5. **Nobody**: the call is rejected. `matimo_approve_tool` is never pre-approved, whatever the patterns or `MATIMO_AUTO_APPROVE` say: approving a tool lets agent-written YAML run, so a person always sees it. An `execute` call that passes a `shell` is held to the same rule, because the command then runs through that shell and pipes, redirection and expansion are live. Nothing skips it: not `MATIMO_AUTO_APPROVE`, not `MATIMO_APPROVED_PATTERNS` (not even `*`), not `approved: true` (including the MCP server's `trustClientApproval`). With no `onApproval` configured the call is rejected. `execute` without a `shell` runs the command directly, with no shell, and is pre-approved like any other tool. ### Per-instance callback (recommended) ```typescript import { MatimoInstance, type ApprovalRequest } from '@matimo/core'; const matimo = await MatimoInstance.init({ autoDiscover: true, onApproval: async (request: ApprovalRequest) => { // request.toolName, request.description, request.params return await askReviewer(request); // true = run it, false = refuse }, }); ``` ```python from matimo import ApprovalRequest, Matimo async def on_approval(request: ApprovalRequest) -> bool: # request.tool_name, request.description, request.params return await ask_reviewer(request) matimo = await Matimo.init(auto_discover=True, on_approval=on_approval) ``` Each instance has its own reviewer, so two tenants in one process never share one. The callback can return `false`/`False` or throw to refuse. ### Per-call callback ```typescript await matimo.execute('delete_post', { id: 1 }, { onApproval: async () => userClickedConfirm }); ``` ```python await matimo.execute("delete_post", {"id": 1}, on_approval=confirm_in_ui) ``` Use it when the reviewer depends on the request, a web handler that already has the user's confirmation, for example. ### Pre-approved patterns ```bash export MATIMO_APPROVED_PATTERNS="get_*,list_*,calculator" ``` Comma-separated globs (`*` matches anything), case-insensitive. A matching tool runs without asking. The variable is read when the process-wide handler is created, so set it before your app starts; in a running process use `getGlobalApprovalHandler().addApprovedPattern('get_*')` (`add_approved_pattern` in Python). ### `MATIMO_AUTO_APPROVE` `MATIMO_AUTO_APPROVE=true` approves every request unseen (except `matimo_approve_tool` and `execute` calls that pass a `shell`); each instance logs a warning when it starts with it on. It is meant for throwaway test environments only. For CI, prefer an `onApproval` that encodes the test's intent, or `MATIMO_APPROVED_PATTERNS` listing the tools the test may run. ### Skipping the prompt for one call `execute(tool, params, { approved: true })` (`approved=True`) skips the approval prompt for that call, for a host that has already confirmed the call through its own UI. It skips **only** the prompt: policy denials, draft gates and HITL quarantine still apply. The one prompt it cannot skip is an `execute` call that passes a `shell`, which always asks. ## What the reviewer sees ```typescript interface ApprovalRequest { toolName: string; // 'github-delete-repository' description?: string; // the tool's description from its YAML params: Record; // the arguments of this call } ``` Python's `ApprovalRequest` dataclass has the same fields in snake_case: `tool_name`, `description`, `params`. ## Outcomes | Outcome | Result | Event | |---------|--------|-------| | Approved | The call runs | `tool:approval_granted` | | Callback returns false | `MatimoError`: `Operation rejected by approval handler: ` | `tool:approval_denied` | | No one to ask | `MatimoError`: `Destructive operation requires approval: `, with a hint naming `onApproval` and `MATIMO_APPROVED_PATTERNS` | `tool:approval_denied` | Events reach `onEvent` and the audit sink; see [Types](https://matimo.dev/docs/api-reference/types.md) for their fields. ## Destructive keywords The keyword scan upper-cases the `sql` argument (or `command`, for command tools) and looks for each keyword as a substring. Set `MATIMO_APPROVAL_SCAN_ALL_PARAMS=true` to scan every string argument instead. Built-in keywords in both SDKs: `CREATE`, `DELETE`, `DROP`, `ALTER`, `TRUNCATE`, `UPDATE`, `INSERT`, `UPSERT`, `REPLACE`, `MERGE`, `GRANT`, `REVOKE`, `EDIT`, `WRITE`, `APPEND`, `REMOVE`, `RENAME`, `SHUTDOWN`, `EXECUTE`, `EXEC`. Python's list also has `DESTROY` and `PURGE`. Because the match is a substring of upper-cased text, a column such as `created_at` contains `CREATE` and makes a `SELECT` ask. Give such a tool its own approval rule rather than relying on the scan. **TypeScript** reads the list from `destructive-keywords.yaml`, looked up in `./packages/core/`, then `./node_modules/@matimo/core/`, then the working directory, falling back to the built-in list. Write keywords in **upper case**: content is upper-cased before matching, so a lower-case entry never matches. **Python** uses the built-in list; change `matimo.get_global_approval_handler().destructive_keywords` to adjust it. ## In MCP servers An MCP server asks the person behind the client through an MCP elicitation request. Clients that can't show one get a clear error. See [MCP](https://matimo.dev/docs/mcp-server.md#approval-over-mcp) for `trustClientApproval` and the server's `context` option. ## Environment variables | Variable | Effect | |----------|--------| | `MATIMO_APPROVED_PATTERNS` | Comma-separated globs of tools that never ask | | `MATIMO_AUTO_APPROVE` | `true` approves everything except `matimo_approve_tool` and `execute` calls with a `shell`, with a warning, test environments only | | `MATIMO_APPROVAL_SCAN_ALL_PARAMS` | `true` scans every string argument for destructive keywords | | `MATIMO_APPROVAL_SECRET` | HMAC secret that signs approvals. Without it each process makes a temporary one and approvals do not persist. Required when `NODE_ENV` or `MATIMO_ENV` is `production` | | `MATIMO_ENV` | Set to `production` to make `MATIMO_APPROVAL_SECRET` mandatory (both SDKs also read `NODE_ENV`) | | `MATIMO_APPROVAL_DIR` | Python only. Directory that holds `.matimo-approvals.json`; defaults to the working directory. In TypeScript use the `approvalDir` option | | `MATIMO_TOOL_DIR` | Folder `matimo review` reads and writes; defaults to the working directory | ## Reviewing agent-created tools from the terminal A tool an agent creates starts as a `draft` and waits for a person. `matimo review` is the command-line way to be that person, in both SDKs: ```bash matimo review list # tools waiting for approval, and tools already approved matimo review approve my_tool # approve one matimo review reject my_tool # reject it, or revoke an earlier approval ``` It looks for `.matimo-approvals.json` and for `/definition.yaml` in the working directory, or in `MATIMO_TOOL_DIR` if set. `approve` refuses to run unless you set `MATIMO_APPROVAL_SECRET` yourself, so the signature comes from a person's secret, not a generated one. It then sets `status: approved` in the definition and records the approval for the definition's current content. Edit the file afterwards and the approval no longer matches, so the tool must be approved again. ## Testing approval ```typescript test('asks before deleting', async () => { const asked: string[] = []; const matimo = await MatimoInstance.init({ toolPaths: ['./tools'], onApproval: async (request) => { asked.push(request.toolName); return false; }, }); await expect(matimo.execute('delete_post', { id: 1 })).rejects.toThrow( 'Operation rejected by approval handler' ); expect(asked).toEqual(['delete_post']); }); ``` ```python async def test_asks_before_deleting() -> None: asked: list[str] = [] async def decline(request: ApprovalRequest) -> bool: asked.append(request.tool_name) return False matimo = await Matimo.init("./tools", on_approval=decline) with pytest.raises(MatimoError, match="rejected by approval handler"): await matimo.execute("delete_post", {"id": 1}) assert asked == ["delete_post"] ``` ## Troubleshooting | Symptom | Cause | Fix | |---------|-------|-----| | `Destructive operation requires approval: ` | The call needs approval and nothing can answer | Pass `onApproval` to `init()`, or pre-approve the tool with `MATIMO_APPROVED_PATTERNS` | | A read-only SQL query asks | The text contains a keyword, e.g. `created_at` | Expected with substring matching; approve it, or use a dedicated read-only tool | | A tool with `requires_approval: false` still asks | The keyword scan found a destructive keyword | Expected: the flag doesn't disable the scan | | A DELETE tool started asking after upgrading | Secure mode, new in 0.2.0 | Add an `onApproval`, set `requires_approval: false` on the tool, or use `governanceMode: 'legacy'` while migrating | | A custom keyword in `destructive-keywords.yaml` never triggers | It is lower-case | Write it in upper case | | `matimo_approve_tool` asks even with `MATIMO_AUTO_APPROVE=true` | By design | A person must approve every tool approval | | `execute` asks even with `MATIMO_AUTO_APPROVE=true` | By design, when the call passes a `shell` | A person must approve every shell call; drop `shell` to run the command directly | ## See Also - [Policy and Lifecycle](https://matimo.dev/docs/api-reference/policy-and-lifecycle.md): risk levels, HITL quarantine, execution gates, migration to 0.2.0 - Runnable demos (no API keys): `typescript/examples/tools/policy/approval-modes-demo.ts`, `python/examples/native/policy/approval_modes_demo.py` - Provider examples: `typescript/examples/tools/postgres/postgres-with-approval.ts`, `typescript/examples/tools/github/github-with-approval.ts` --- # Meta-Tools Reference Source: https://matimo.dev/docs/api-reference/meta-tools/ > Complete reference for all built-in Matimo meta-tools, the tools that manage other tools. ## Overview Meta-tools are built-in tools that live in `packages/core/tools/` and provide tool lifecycle management capabilities. They allow agents (LangChain, MCP, SDK) to create, validate, approve, reload, and discover tools at runtime. | Meta-Tool | Purpose | Requires Approval | |-----------|---------|:-----------------:| | [`matimo_validate_tool`](#matimo_validate_tool) | Validate YAML against schema + policy rules | No | | [`matimo_create_tool`](#matimo_create_tool) | Write a new tool definition to disk | Yes | | [`matimo_approve_tool`](#matimo_approve_tool) | Promote a draft tool to approved status | Yes | | [`matimo_reload_tools`](#matimo_reload_tools) | Hot-reload all tools into the live registry | Yes | | [`matimo_list_user_tools`](#matimo_list_user_tools) | List tools in a directory with metadata | No | | [`matimo_get_tool_status`](#matimo_get_tool_status) | Get status, risk level, and approval state of a tool | Yes | | [`matimo_get_tool`](#matimo_get_tool) | Retrieve a tool's full YAML + parsed definition | No | | [`matimo_search_tools`](#matimo_search_tools) | Search the loaded tool registry by keyword | No | | [`matimo_create_skill`](#matimo_create_skill) | Create a SKILL.md file with validated frontmatter | Yes | | [`matimo_list_skills`](#matimo_list_skills) | List skills in a directory with metadata | No | | [`matimo_get_skill`](#matimo_get_skill) | Read a skill's full content by name | No | | [`matimo_validate_skill`](#matimo_validate_skill) | Validate a skill against the Agent Skills spec | No | | [`matimo_search_skills`](#matimo_search_skills) | Search skills by keyword relevance (TF-IDF) | No | | [`matimo_get_skill_sections`](#matimo_get_skill_sections) | List a skill's section inventory with token estimates | No | | [`matimo_get_skill_content`](#matimo_get_skill_content) | Load a skill's content, optionally scoped to specific sections | No | > **No `matimo_doctor` or `matimo_review` meta-tool exists.** Older prompts and docs used those names. Agents validate with `matimo_validate_tool` and request approval with `matimo_approve_tool`. `matimo doctor` (diagnose a setup) and `matimo review` (list, approve or reject agent-created tools) are **CLI commands** for people, not tools an agent can call. > **New to meta-tools?** See [When to Use Which Meta-Tool](#when-to-use-which-meta-tool) for decision guides and typical agent workflows before diving into individual tool references. **Common tags:** All meta-tools are tagged with `matimo` and `meta`. --- ## When to Use Which Meta-Tool ### Decision Guide: Tool Lifecycle | I want to... | Use | | --- | --- | | Check if my YAML is safe before doing anything | `matimo_validate_tool` | | Write a new tool to disk | `matimo_create_tool` | | Promote a draft to production-ready | `matimo_approve_tool` | | Make newly created/approved tools available | `matimo_reload_tools` | | See what tools an agent has created | `matimo_list_user_tools` | | Check a specific tool's approval state | `matimo_get_tool_status` | | Read a tool's full YAML before editing/cloning | `matimo_get_tool` | | Find a tool by keyword without listing everything | `matimo_search_tools` | ### Decision Guide: Skills Lifecycle | I want to... | Use | | --- | --- | | Discover what skills are available | `matimo_list_skills` | | Find the most relevant skill by meaning | `matimo_search_skills` | | Read the full content of a skill | `matimo_get_skill` | | See a skill's sections before loading it whole | `matimo_get_skill_sections` | | Load only specific sections of a skill | `matimo_get_skill_content` | | Create a new SKILL.md at runtime | `matimo_create_skill` | | Check if a skill follows the Agent Skills spec | `matimo_validate_skill` | ### Use Cases by Meta-Tool | Meta-Tool | When to Use | What Happens If You Skip It | |-----------|-------------|-----------------------------| | `matimo_validate_tool` | Before `matimo_create_tool`, catch errors early without writing to disk | Tool creation may fail mid-write with a less clear error | | `matimo_create_tool` | Agent proposes a new capability (weather lookup, data fetch) | Tool never exists; agent can't use the new capability | | `matimo_approve_tool` | After creation, a human promotes the draft to `approved` | Tool stays a draft: it loads, but every call fails with `Draft tool ... requires admin role` | | `matimo_reload_tools` | After create+approve, make new tools available without restart | Agent can't call the new tool until the process is restarted | | `matimo_list_user_tools` | Agent wants to audit what it has created this session | Agent re-creates duplicates, wastes tool slots | | `matimo_get_tool_status` | Before using a tool the agent created, verify it's approved | Agent calls a draft tool and hits an approval gate error | | `matimo_get_tool` | Before editing or cloning an existing tool, inspect its full YAML | Agent guesses at the schema and may overwrite/duplicate incompatibly | | `matimo_search_tools` | Agent needs to find a relevant tool without listing the entire registry | Agent misses an existing tool and creates a redundant duplicate | | `matimo_list_skills` | At session start or when agent needs domain knowledge | Agent misses available expertise, gives generic responses | | `matimo_get_skill` | When agent needs specific domain knowledge for a task | Agent works without guidelines, prone to API misuse | | `matimo_create_skill` | Team wants to package reusable agent expertise | Knowledge scattered in system prompts, not reusable | | `matimo_validate_skill` | After creating a skill, verify spec compliance | Skill may fail to load or have invalid frontmatter silently | | `matimo_search_skills` | Agent needs to find the right skill by meaning, not exact name | Agent falls back to `matimo_list_skills` and guesses from titles alone | | `matimo_get_skill_sections` | Before loading a large skill, check its shape and size first | Agent loads the whole skill and burns context budget on irrelevant sections | | `matimo_get_skill_content` | Agent only needs specific sections of a skill | Agent must load (and pay for) the entire skill via `matimo_get_skill` | ### Typical Agent Workflows **Workflow A, Agent creates and uses a new tool (full lifecycle)** ``` 1. matimo_validate_tool → Check the YAML would be accepted (no approval needed) 2. matimo_create_tool → Write the draft to disk (needs human ) 3. matimo_approve_tool → A human promotes it to approved (always asks ) 4. matimo_reload_tools → Load it into the live registry (needs human ) 5. matimo.execute(name) → Use the tool (requires_approval: true, so each call asks ) ``` Calling the tool between steps 2 and 3 (after a reload) fails: a draft runs only for an `admin` caller outside production. **Workflow B, Agent discovers and applies skills** ``` 1. matimo_list_skills → See available skills (free, no approval) 2. matimo_get_skill → Load the relevant one (free, no approval) 3. Agent applies guidelines from skill content in its response ``` **Workflow C, Agent audits what it built** ``` 1. matimo_list_user_tools → List all tools created this session 2. matimo_get_tool_status → Check approval state for each tool 3. matimo_reload_tools → Ensure approved tools are live ``` **Workflow D, Agent discovers before creating (avoid duplicates)** ``` 1. matimo_search_tools → Check if a similar tool already exists (free, no approval) 2. matimo_get_tool → Inspect the full YAML if a close match is found (free, no approval) 3. matimo_create_tool → Only create if nothing suitable exists (needs human ) ``` **Workflow E, Agent discovers and selectively loads a skill (progressive disclosure)** ``` 1. matimo_search_skills → Find the most relevant skill by meaning (free, no approval) 2. matimo_get_skill_sections → Check the skill's section inventory and token estimates (free, no approval) 3. matimo_get_skill_content → Load only the sections needed for the task (free, no approval) ``` --- ## matimo_validate_tool Validate a tool definition YAML string against the Matimo schema and policy rules. Returns schema errors, policy violations, and risk classification. `valid` is `true` exactly when `matimo_create_tool` would accept the definition: the check applies the fields creation forces (`requires_approval: true`, `status: draft`), so leave those out of the YAML. **Does not require approval**, safe for agents to call freely. ### Parameters | Parameter | Type | Required | Description | |-----------|------|:--------:|-------------| | `yaml_content` | string | Yes | The YAML content of the tool definition to validate | ### Response ```typescript { valid: boolean; // true when matimo_create_tool would accept it schemaErrors: Array<{ field: string; // e.g. 'execution.method' message: string; validOptions?: string[]; // for enum fields }>; policyViolations: Array<{ rule: string; // e.g., 'no-command-execution' severity: string; // 'critical' | 'high' | 'medium' | 'low' message: string; // Human-readable explanation }>; riskLevel: string; // risk of the definition as written: 'low' | 'medium' | 'high' | 'critical' } ``` ### Example ```typescript const result = await matimo.execute('matimo_validate_tool', { yaml_content: ` name: my_api_tool version: '1.0.0' description: Fetch data from an API parameters: query: type: string required: true execution: type: http method: GET url: 'https://api.example.com/search?q={query}' `, }); // { // valid: true, // schemaErrors: [], // policyViolations: [], // riskLevel: 'low' // } ``` ### What It Checks 1. **YAML syntax**: can the content be parsed? 2. **Schema validation**: does it match the `ToolDefinition` Zod schema? (name, version, description, execution, parameters) 3. **Content rules**: runs the 9 content validator rules with the **default** policy (see [POLICY_AND_LIFECYCLE.md](https://matimo.dev/docs/api-reference/policy-and-lifecycle.md#9-security-rules)). Only critical and high violations make it invalid. The running instance's `allowedDomains`, `allowedHttpMethods` and `allowedCredentials` apply when the tool loads, so check `rejected` from `matimo_reload_tools` too. 4. **Risk classification**: assigns a risk level from the execution type and HTTP method ### Error Cases | Scenario | `valid` | Response | |----------|:-------:|----------| | Invalid YAML syntax | `false` | `schemaErrors: [{ field: "root", message: "YAML parse error: ..." }]` | | Missing required fields | `false` | `schemaErrors: [{ field: "version", message: "..." }]` | | Command tool (blocked by policy) | `false` | `policyViolations: [{ rule: "no-command-execution", severity: "critical" }]` | | SSRF attempt | `false` | `policyViolations: [{ rule: "no-ssrf", severity: "critical" }]` | | Valid HTTP GET tool | `true` | `riskLevel: "low"` | --- ## matimo_create_tool Create a new tool definition on disk. Validates the YAML, forces draft status and `requires_approval: true`, and writes the tool to the target directory. **Requires approval**, human must confirm before tool is written to disk. ### Parameters | Parameter | Type | Required | Default | Description | |-----------|------|:--------:|---------|-------------| | `name` | string | Yes | n/a | Name for the new tool (snake_case) | | `yaml_content` | string | Yes | n/a | The YAML content of the tool definition | | `target_dir` | string | No | `./matimo-tools` | Directory to create the tool in | | `proposed_by` | string | No | n/a | Identifier of who proposed this tool | | `justification` | string | No | n/a | Reason for creating this tool | ### Response ```typescript // Success { success: true, path: './agent-tools/weather/definition.yaml', riskLevel: 'high', // requires_approval: true raises an HTTP tool to high status: 'draft', approvalState: 'pending', // always, for a new tool message: 'Tool created as a draft (low risk, read-only). It runs after a reviewer approves it with matimo_approve_tool and the tools reload.' } // Failure { success: false, message: 'Tool failed policy validation', errors: ['[critical] no-command-execution: Command-type tools are not allowed'] } ``` ### Example ```typescript const result = await matimo.execute('matimo_create_tool', { name: 'city_lookup', target_dir: './agent-tools', proposed_by: 'agent-1', justification: 'User requested city information lookup', yaml_content: ` name: city_lookup version: '1.0.0' description: Look up user information including city and address details parameters: id: type: string required: true description: User ID to look up (1-10) execution: type: http method: GET url: 'https://jsonplaceholder.typicode.com/users/{id}' `, }); ``` ### Safety Enforcement The following fields are **always forced** regardless of what the YAML contains: | Field | Forced Value | Why | |-------|-------------|-----| | `name` | `params.name` | Prevents name mismatch | | `requires_approval` | `true` | Agent-created tools must be approved | | `status` | `'draft'` | Agent-created tools start as draft | ### Approval State After Creation Every created tool is a draft, so `approvalState` is always `'pending'`: the tool runs only after a human approves it with `matimo_approve_tool` and the tools reload. The message says whether the definition is low-risk and read-only (an HTTP GET without credentials), which helps the reviewer, but it does not skip the review. | Execution Type | Method | `riskLevel` after create | `approvalState` after create | |---------------|--------|--------------------------|------------------------------| | `http` | any | `high` (`requires_approval: true` is forced) | `pending` | | `command` / `function` | n/a | refused by policy | n/a | ### Name Validation The tool name is sanitized to prevent security issues: | Check | Blocked Pattern | Example | |-------|----------------|---------| | Path traversal | `../`, `..\\` | `../../etc/passwd` | | Backslash | `\` | `tools\backdoor` | | Control characters | `\x00`-`\x1f` | Null bytes, newlines | | Reserved namespace | `matimo_*` | `matimo_backdoor` | | Empty/whitespace | `""`, `" "` | Blank names | ### Internal Flow 1. Sanitize name (reject invalid patterns) 2. Parse YAML (reject syntax errors) 3. Force `name`, `requires_approval: true`, `status: 'draft'` 4. Validate against `ToolDefinition` Zod schema 5. Run content validator (9 rules) 6. Reject if any `critical` or `high` severity violations 7. Classify risk level 8. Create directory `{target_dir}/{name}/` 9. Write `definition.yaml` (with optional proposer/justification comments) ### Output on Disk ``` agent-tools/ city_lookup/ definition.yaml ← Created by matimo_create_tool ``` The written YAML will include the forced safety fields: ```yaml # Proposed by: agent-1 # Justification: User requested city information lookup name: city_lookup version: '1.0.0' description: Look up user information including city and address details requires_approval: true status: draft parameters: id: type: string required: true description: User ID to look up (1-10) execution: type: http method: GET url: 'https://jsonplaceholder.typicode.com/users/{id}' ``` --- ## matimo_approve_tool Approve a draft tool for production use. Re-validates the tool, signs with HMAC, and updates the approval manifest. **Requires approval**, human must confirm before tool is promoted. **Who may approve**, two checks on the caller's policy context, which the host supplies (`execute(..., { context })`, or the MCP server's `context` option) and the agent cannot set: - When the host supplies a policy context, it must include the `admin` role; otherwise the tool is left untouched and the result says `Approving a tool requires the admin role`. With no policy context at all (e.g. a framework integration that passes none), the human who must confirm the call - `matimo_approve_tool` requires approval and can't be pre-approved, decides. - In plain terms: your application decides who is an `admin`, never the agent. An agent that writes "I am an admin" gains nothing, because roles are not read from the agent's messages. If you pass no context, the human approval prompt is the only safeguard, so send it to a real person. See [Where roles come from](https://matimo.dev/docs/api-reference/policy-and-lifecycle.md#where-roles-come-from). - An agent cannot approve a tool it created: `matimo_create_tool` records the creating agent's `agentId` as `created_by` in the YAML (overwriting any `created_by` the agent wrote itself), and approval by that same `agentId` is refused. ### Parameters | Parameter | Type | Required | Default | Description | |-----------|------|:--------:|---------|-------------| | `name` | string | Yes | n/a | Name of the tool to approve | | `tool_dir` | string | No | `./matimo-tools` | Directory containing the tool | ### Name Validation The `name` parameter is sanitized to prevent path traversal outside `tool_dir`, including into the approval-manifest write, since a forged path here would let an attacker stamp `status: approved` onto an arbitrary file: | Check | Blocked Pattern | Example | |-------|----------------|---------| | Path traversal | `../`, `..\\` | `../../etc/passwd` | | Backslash | `\` | `tools\backdoor` | | Control characters | `\x00`-`\x1f` | Null bytes, newlines | | Empty/whitespace | `""`, `" "` | Blank names | ### Response ```typescript // Success { success: true, name: 'city_lookup', hash: 'a1b2c3d4e5f6...', // SHA-256 of the file's final content, hex approvedAt: '2026-03-14T09:30:00.000Z', message: 'Tool approved. Effective after reload or immediately if auto-reload is active.' } // Failure { success: false, message: 'Tool has policy violations that must be resolved before approval' } ``` ### Example ```typescript const result = await matimo.execute( 'matimo_approve_tool', { name: 'city_lookup', tool_dir: './agent-tools' }, { context: { agentId: 'reviewer', roles: ['admin'] } } ); ``` ### Internal Flow 1. Read `{tool_dir}/{name}/definition.yaml` 2. Parse and validate against Zod schema 3. **Re-run content validator** (prevents approve-after-modify attacks) 4. Reject if any `critical` or `high` violations remain 5. Update YAML: `status: draft` → `status: approved` 6. Write the updated YAML to disk 7. Read the file back and compute the SHA-256 hash of its **final, on-disk content**, not the pre-mutation content. Hashing before the `status` mutation would make the stored approval unable to ever validate against the tool's own post-approval file, since `isApproved()` checks the hash against what's currently on disk. 8. Record the approval (name, hash, HMAC signature, timestamp) in the approval manifest of the instance that owns the call, the instance passed to `setGlobalMatimoInstance()` (in Python, the instance executing the call), so its next reload sees it. That manifest lives in the instance's `approvalDir` and signs with `MATIMO_APPROVAL_SECRET`, or an ephemeral secret (with a warning) when unset. With no global instance, the tool writes its own manifest in `tool_dir`. ### Approval Manifest File `.matimo-approvals.json` in the instance's `approvalDir` (or `tool_dir` when no instance is registered): ```json { "city_lookup": { "hash": "a1b2c3d4e5f6...", "signature": "...", "approvedAt": "2026-03-14T09:30:00.000Z", "approvedBy": "system" } } ``` ### Tamper Detection If the YAML is modified after approval: 1. On the next `reloadTools()` the hash is recomputed 2. New hash ≠ stored hash → the approval no longer counts, and the tool is checked as a new proposal (a file still saying `status: approved` is then rejected) 3. The tool must be re-approved Without `MATIMO_APPROVAL_SECRET`, approvals last only for the current process. Set it to keep them across restarts, or to approve from another process with the `matimo review approve ` CLI (it must use the same secret and approval directory as the running instance). --- ## matimo_reload_tools Hot-reload all tools from configured `toolPaths`. Clears the registry, re-reads YAML definitions from disk, re-validates untrusted tools against the active policy, and returns a summary. **Requires approval**, human must confirm before registry is rebuilt. ### Parameters No parameters required. The tool uses the `toolPaths` and `untrustedPaths` configured during `MatimoInstance.init()`. ### Response ```typescript { success: true, loaded: 13, // Tools registered by this reload (trusted and untrusted) removed: 0, // Tools that were in the registry but are no longer loaded revalidated: 1, // Untrusted tools that were re-checked against policy rejected: [], // Tool names that failed policy and were not loaded message: 'Reload complete. 13 tools loaded, 0 removed, 0 rejected.' } ``` ### Example ```typescript // Via meta-tool (works from SDK, LangChain, and MCP) const result = await matimo.execute('matimo_reload_tools', {}); // Or programmatically (SDK only) const reloadResult = await matimo.reloadTools(); ``` ### How Reload Distinguishes "Already Approved" from "New Proposal" Every untrusted tool is re-validated on reload, but which gate it's checked against depends on whether it was legitimately approved: 1. Hash the tool's current on-disk YAML. 2. If that hash matches a signed record in the approval manifest (i.e. it went through `matimo_approve_tool` and hasn't been modified since), it's checked with the looser `canReload()`/`can_reload()` gate, which skips only the two rules whose purpose is "a *new proposal* cannot self-declare approval/non-draft status," since a real approval legitimately changed those fields. All other content rules (SSRF, credentials, namespace, HTTP method/domain) still apply in full. 3. Otherwise, including a tool hand-edited to `status: approved` without ever going through `matimo_approve_tool`, it falls back to the stricter `canCreate()`/`can_create()` gate, the same one used for brand-new proposals. This is what keeps the anti-self-approval hole closed: forging `status: approved` in the YAML directly, with no matching manifest record, still gets rejected on reload. Without this distinction, a legitimately approved tool's own post-approval `status`/`requires_approval` fields would trip the "new proposal" rules on every subsequent reload, and the tool could never actually be used, the create → approve → reload → execute lifecycle would silently fail on its own second step. ### Why This Meta-Tool Exists Without `matimo_reload_tools`, the create→approve→reload→use lifecycle could only be completed from SDK code (`matimo.reloadTools()`). MCP clients and LangChain agents had no way to trigger a reload. With this meta-tool: | Interface | Reload Method | |-----------|:-------------| | SDK | `matimo.reloadTools()` or `matimo.execute('matimo_reload_tools', {})` | | LangChain | Agent calls `matimo_reload_tools` as a tool | | MCP | Client calls `tools/call` with `name: "matimo_reload_tools"` | ### Internal Flow (Interception Pattern) This tool uses a special interception pattern. Unlike other meta-tools that execute via the `FunctionExecutor`, `matimo_reload_tools` is intercepted directly in `MatimoInstance.execute()`: ![matimo_reload_tools passes the policy and approval checks, is intercepted before executor routing, calls reloadTools directly on the instance, and returns a ReloadResult as the tool response.](../../../../assets/diagrams/meta-reload-light.svg) ![matimo_reload_tools passes the policy and approval checks, is intercepted before executor routing, calls reloadTools directly on the instance, and returns a ReloadResult as the tool response.](../../../../assets/diagrams/meta-reload-dark.svg) **Why interception?** The `FunctionExecutor` doesn't have a reference to the `MatimoInstance`, so it can't call `this.reloadTools()`. The interception happens after all policy and approval checks, so security is fully enforced. ### After Reload (LangChain) After calling `matimo_reload_tools`, LangChain agents must rebind their tools: ```typescript // Reload registry await matimo.execute('matimo_reload_tools', {}); // Rebind LangChain tools (registry changed) const updatedTools = matimo.listTools() as ToolDefinition[]; const langchainTools = await convertToolsToLangChain(updatedTools, matimo); llmWithTools = llm.bindTools(langchainTools); ``` ### After Reload (MCP) MCP reload automatically sends a `notifications/tools/list_changed` notification to connected clients, prompting them to re-fetch the tool list. --- ## matimo_get_tool_status Get the current status, risk level, and approval state of a specific tool by name. Works for both draft and approved tools. **Requires approval**, reads from the approval manifest (sensitive metadata). ### Parameters | Parameter | Type | Required | Default | Description | |-----------|------|:--------:|---------|-------------| | `name` | string | Yes | n/a | Name of the tool to check | | `tool_dir` | string | No | `./matimo-tools` | Directory containing the tool | ### Name Validation The `name` parameter is sanitized to prevent path traversal outside `tool_dir`: | Check | Blocked Pattern | Example | |-------|----------------|---------| | Path traversal | `../`, `..\\` | `../../etc/passwd` | | Backslash | `\` | `tools\backdoor` | | Control characters | `\x00`-`\x1f` | Null bytes, newlines | | Empty/whitespace | `""`, `" "` | Blank names | ### Response ```typescript // Tool found { found: true, name: 'weather_fetch', status: 'approved', // the YAML's status riskLevel: 'high', // 'low' | 'medium' | 'high' | 'critical' approvalState: 'approved', // see the table below approvedAt: '2026-03-14T09:30:00.000Z', approvedBy: undefined, message: 'Tool "weather_fetch" is approved (high risk)' } // Tool not found { found: false, name: 'nonexistent', message: 'Tool "nonexistent" not found at ./matimo-tools/nonexistent/definition.yaml' } ``` | `approvalState` | Meaning | |-----------------|---------| | `pending` | A draft not yet approved, or a tool that needs approval and has none | | `approved` | A valid approval record matches the file's current content | | `auto-approved` | A low-risk, read-only tool that is not a draft | | `rejected` | The tool is deprecated | ### Example ```typescript const result = await matimo.execute('matimo_get_tool_status', { name: 'weather_fetch', tool_dir: './agent-tools', }); if (result.found) { console.log(`Status: ${result.status}, Risk: ${result.riskLevel}`); } ``` --- ## matimo_get_tool Retrieve the full definition of a tool, both the raw YAML source and its parsed, schema-validated fields. Use this before editing or cloning a tool, so the agent works from the actual definition rather than guessing at its shape. **Does not require approval**, read-only operation. ### Parameters | Parameter | Type | Required | Default | Description | |-----------|------|:--------:|---------|-------------| | `name` | string | Yes | n/a | Name of the tool to retrieve | | `tool_dir` | string | No | `./matimo-tools` | Directory containing the tool | ### Name Validation The `name` parameter is sanitized to prevent path traversal outside `tool_dir`, without this, a crafted name could read arbitrary files on disk (e.g. `../../.env`) and return their contents as `yaml_content`: | Check | Blocked Pattern | Example | |-------|----------------|---------| | Path traversal | `../`, `..\\` | `../../etc/passwd` | | Backslash | `\` | `tools\backdoor` | | Control characters | `\x00`-`\x1f` | Null bytes, newlines | | Empty/whitespace | `""`, `" "` | Blank names | ### Response ```typescript // Found, valid YAML { found: true, name: 'weather_fetch', yaml_content: 'name: weather_fetch\ndescription: ...\n...', definition: { name: 'weather_fetch', description: '...', parameters: { ... }, execution: { ... } }, message: 'Tool "weather_fetch" retrieved successfully' } // Found, but the YAML fails schema validation { found: true, name: 'broken_tool', yaml_content: 'name: broken_tool\n...', message: 'Tool YAML is invalid: ' } // Not found { found: false, message: 'Tool "nonexistent" not found at ./matimo-tools/nonexistent/definition.yaml' } ``` ### Example ```typescript const result = await matimo.execute('matimo_get_tool', { name: 'weather_fetch', tool_dir: './agent-tools', }); if (result.found && result.definition) { console.log(result.definition.parameters); } ``` ### Notes - `definition` is only present when the on-disk YAML parses and passes schema validation; the internal `_definitionPath` field is stripped from the returned object. - `yaml_content` is always returned when the tool is found, even if the definition itself is invalid, useful for surfacing the raw source to fix errors. --- ## matimo_search_tools Search the loaded tool registry by keyword. Matches against tool names, descriptions, and tags, and returns each match's risk level. Use this to discover whether a suitable tool already exists before creating a new one. **Does not require approval**, read-only operation. ### Parameters | Parameter | Type | Required | Default | Description | |-----------|------|:--------:|---------|-------------| | `query` | string | Yes | n/a | Search keyword, matched against tool names, descriptions, and tags | | `limit` | number | No | `20` | Maximum number of results to return | ### Response ```typescript { results: [ { name: 'gmail-send-email', description: 'Send an email via the Gmail API', version: '1.0.0', tags: ['gmail', 'email'], riskLevel: 'medium' } ], total: 1, query: 'email' } ``` ### Example ```typescript const result = await matimo.execute('matimo_search_tools', { query: 'email', limit: 5, }); // result.results → matching tools with name, description, version, tags, riskLevel ``` ### Internal Flow 1. If a global Matimo instance is registered, or in Python the instance executing the call (normal runtime case), search its in-memory tool registry directly, covers every loaded tool across all packages. 2. If no global instance is available (e.g. called standalone), fall back to scanning `./matimo-tools` on disk the same way `matimo_list_user_tools` does, matching each tool's `definition.yaml` against the query. 3. Results are capped at `limit` in both paths. --- ## matimo_list_user_tools List all user-created tools in a directory with risk classification, approval status, and metadata. **Does not require approval**, safe for agents to call freely. ### Parameters | Parameter | Type | Required | Default | Description | |-----------|------|:--------:|---------|-------------| | `tool_dir` | string | No | `./matimo-tools` | Directory to scan for tools | | `include_drafts` | boolean | No | `true` | Whether to include draft tools | ### Response ```typescript { tools: [ { name: 'city_lookup', description: 'Look up city information', version: '1.0.0', status: 'approved', riskLevel: 'low', tags: [] }, { name: 'weather', description: 'Get current weather', version: '1.0.0', status: 'draft', riskLevel: 'low', tags: ['weather'] } ], total: 2 } ``` ### Example ```typescript // List all tools (including drafts) const result = await matimo.execute('matimo_list_user_tools', { tool_dir: './agent-tools', }); // List only approved tools const approvedOnly = await matimo.execute('matimo_list_user_tools', { tool_dir: './agent-tools', include_drafts: false, }); ``` ### Internal Flow 1. Check if `tool_dir` exists (return empty if not) 2. Scan directory entries 3. For each subdirectory, look for `definition.yaml` 4. Parse YAML + validate against schema 5. Classify risk level 6. Filter by `include_drafts` flag 7. Return tool summaries ### Error Handling - If a tool's YAML is invalid, it's skipped with a warning log (doesn't fail the entire listing) - If `tool_dir` doesn't exist, returns `{ tools: [], total: 0 }` --- ## matimo_create_skill Create a new skill definition (SKILL.md) on disk. Validates the YAML frontmatter against the Agent Skills spec, writes the file, and emits a `skill:created` event on the owning instance. **Requires approval**, human must confirm before skill is written. The skill is not in the registry until the instance reloads its skills (`reloadSkills()`, or `addSkillPath()` + `reloadSkills()` for a new directory). ### Parameters | Parameter | Type | Required | Default | Description | |-----------|------|:--------:|---------|-------------| | `name` | string | Yes | n/a | Skill directory name: lowercase letters, digits and single hyphens, at most 64 characters, matching the frontmatter `name` | | `content` | string | Yes | n/a | Markdown content with YAML frontmatter | | `target_dir` | string | No | the instance's `defaultSkillWriteDir`, else `./matimo-tools/skills` | Directory to create the skill in | ### Response ```typescript // Success { success: true, path: './matimo-tools/skills/my-skill/SKILL.md', message: 'Skill "my-skill" created successfully.' } // Failure { success: false, message: 'YAML frontmatter must include a "description" field' } ``` ### Example ```typescript const result = await matimo.execute('matimo_create_skill', { name: 'data-analysis', content: `--- name: data-analysis description: Analyze datasets using statistical methods --- # Data Analysis Skill This skill enables statistical analysis of tabular datasets. ## Capabilities - Descriptive statistics (mean, median, mode, std deviation) - Correlation analysis - Trend detection `, }); ``` ### Frontmatter Requirements The content must start with YAML frontmatter (`---`) containing at least: | Field | Required | Description | |-------|:--------:|-------------| | `name` | Yes | Skill name, the same as the `name` parameter | | `description` | Yes | What the skill does | Optional fields from the spec: `license`, `compatibility`, `metadata`, `allowed-tools`. ### Name Validation Lowercase letters, digits and hyphens only; no leading, trailing or consecutive hyphens; at most 64 characters. This also rules out path traversal, backslashes and control characters. ### Output on Disk ``` matimo-tools/ skills/ data-analysis/ SKILL.md ← Created by matimo_create_skill ``` --- ## matimo_list_skills List all skills (SKILL.md files) in a directory. Returns each skill's name, description, and file path from its YAML frontmatter. **Does not require approval**, read-only operation. ### Parameters | Parameter | Type | Required | Default | Description | |-----------|------|:--------:|---------|-------------| | `skills_dir` | string | No | n/a | Extra directory to scan; its skills are added to the ones below | Which skills are listed: the global instance's skills (in Python, the instance executing the call), which is everything it loaded plus any registered with `registerSkill()`; with no instance, the skills shipped in installed `@matimo/*` packages. Skills in `skills_dir` are always added. ### Response ```typescript { skills: [ { name: 'code-review', description: 'Code review checklist and best practices', version: '1.0.0', // optional frontmatter fields when present license: 'MIT', metadata: { category: 'Quality' }, source: 'user' // 'builtin' | 'user' | 'catalog' } ], total: 1 } ``` ### Example ```typescript const result = await matimo.execute('matimo_list_skills', { skills_dir: './my-skills', }); // result.skills → array of { name, description, source, ...optional frontmatter } // result.total → number of skills found ``` Skills with missing or invalid frontmatter are skipped. --- ## matimo_get_skill Read the full content of a skill (SKILL.md) by name. Returns the skill's frontmatter metadata and complete markdown content so the agent can use it as instructions or context. **Does not require approval**, read-only operation. ### Parameters | Parameter | Type | Required | Default | Description | |-----------|------|:--------:|---------|-------------| | `name` | string | Yes | n/a | Name of the skill (matches the skill's directory name) | | `skills_dir` | string | No | `./matimo-tools/skills` | Directory containing skills | ### Response ```typescript // Success { success: true, name: 'code-review', description: 'Code review checklist and best practices', content: '---\nname: code-review\ndescription: ...\n---\n\n# Code Review Checklist\n...', path: './matimo-tools/skills/code-review/SKILL.md', resources: { scripts: [], references: [], assets: [], other: [] }, // bundled files message: 'Skill retrieved successfully.' } // Failure { success: false, message: 'Skill "nonexistent" not found at ./matimo-tools/skills/nonexistent/SKILL.md' } ``` ### Example ```typescript const result = await matimo.execute('matimo_get_skill', { name: 'code-review', skills_dir: './my-skills', }); if (result.success) { // The agent can now read result.content and apply the skill's guidelines console.log(result.content); } ``` ### Name Validation Spec-compliant name rules: - Lowercase letters, numbers, and hyphens only - 1–64 characters - No leading/trailing hyphens - No consecutive hyphens - Must match the skill's directory name --- ## matimo_validate_skill Validate an existing skill against the [Agent Skills specification](https://agentskills.io/specification). Checks SKILL.md existence, frontmatter validity, name rules, and directory structure. **Does not require approval**, read-only validation. ### Parameters | Parameter | Type | Required | Default | Description | |-----------|------|:--------:|---------|-------------| | `name` | string | Yes | n/a | Name of the skill to validate | | `skills_dir` | string | No | `./matimo-tools/skills` | Directory containing skills | ### Response ```typescript // Valid skill { valid: true, name: 'code-review', issues: [], structure: { has_skill_md: true, resources: { scripts: [], references: [], assets: [], other: [] } }, message: 'Skill "code-review" is valid.' } // Invalid skill { valid: false, name: 'Bad_Name', issues: [ { field: 'name', severity: 'error', message: 'Skill name must contain only lowercase letters, numbers, and hyphens, and must not start or end with a hyphen' } ], structure: { has_skill_md: true, resources: { ... } }, message: 'Skill "Bad_Name" has 1 error(s).' } ``` ### Example ```typescript const result = await matimo.execute('matimo_validate_skill', { name: 'code-review', skills_dir: './my-skills', }); if (result.valid) { console.log('Skill is spec-compliant!'); } else { console.log('Issues:', result.issues); } ``` --- ## matimo_search_skills Relevance search across all loaded skills by natural language query. Ranks skills by keyword overlap (TF-IDF by default, or a custom embedding provider if one is configured). Use this to discover which skill to load before calling `matimo_get_skill` or `matimo_get_skill_content`. **Does not require approval**, read-only operation. ### Parameters | Parameter | Type | Required | Default | Description | |-----------|------|:--------:|---------|-------------| | `query` | string | Yes | n/a | Natural language search query (e.g. `"rate limiting and retries"`) | | `limit` | number | No | `10` | Maximum number of results to return | | `min_score` | number | No | `0.1` | Minimum cosine similarity score (0-1) a result must meet | ### Response ```typescript { success: true, query: 'rate limiting and retries', results: [ { name: 'slack', description: 'Slack API integration patterns', relevanceScore: 0.42 }, ], total: 1, message: 'Found 1 matching skill(s).' } ``` ### Example ```typescript const result = await matimo.execute('matimo_search_skills', { query: 'rate limiting and retries', limit: 5, }); // result.results → ranked matches with { name, description, relevanceScore } ``` ### Internal Flow 1. Requires an active global Matimo instance (via `setGlobalMatimoInstance`/`set_global_matimo_instance`, same as the `@tool` decorator; in Python the instance executing the call counts, so `matimo.execute('matimo_search_skills', ...)` needs no registration), returns a clear failure message if none is available. 2. Delegates to `MatimoInstance.semanticSearchSkills()` / `Matimo.semantic_search_skills()`, which ranks by TF-IDF cosine similarity over each skill's `name` + `description` **only**, the skill `body` is not indexed for ranking. 3. Flattens each `{ skill, score }` hit into `{ name, description, relevanceScore }` and truncates to `limit`. > **Note:** because ranking is based on name+description text overlap, a near-duplicate corpus (skills with very similar descriptions) can collapse TF-IDF's IDF term to near-zero for every result, a varied corpus produces more meaningful scores. --- ## matimo_get_skill_sections Inventory a skill's Markdown sections and their approximate token costs, without loading the full content. Progressive disclosure Level 2.5, use this to decide which sections to load via `matimo_get_skill_content` before spending context budget on the whole file. **Does not require approval**, read-only operation. ### Parameters | Parameter | Type | Required | Description | |-----------|------|:--------:|-------------| | `name` | string | Yes | Name of the skill to inspect (must match a loaded skill's name) | ### Response ```typescript // Success { success: true, name: 'slack', sections: [ { path: 'Slack', level: 1, tokenEstimate: 40 }, { path: 'Slack.Messaging', level: 2, tokenEstimate: 340 }, { path: 'Slack.Messaging.Threads', level: 3, tokenEstimate: 120 }, { path: 'Slack.Error Handling', level: 2, tokenEstimate: 210 }, ], total: 4, message: 'Found 4 section(s) for skill "slack".' } // Skill not found { success: false, name: 'nonexistent', sections: [], total: 0, message: 'Skill "nonexistent" not found' } ``` ### Example ```typescript const result = await matimo.execute('matimo_get_skill_sections', { name: 'slack' }); // result.sections → { path, level, tokenEstimate } per heading, in document order ``` ### Internal Flow 1. Requires an active global Matimo instance, same requirement as `matimo_search_skills`. 2. Delegates to `MatimoInstance.getSkillSections()` / `Matimo.get_skill_sections()`, which walks the skill's parsed Markdown heading tree and returns each section's dotted heading path, nesting level, and a word-count-based token estimate. 3. Returns `null` from the underlying method (surfaced here as `success: false`) if no loaded skill matches `name`. --- ## matimo_get_skill_content Load only specific sections of a skill instead of the entire SKILL.md, token-efficient context loading. Pair with `matimo_get_skill_sections` to first inventory a skill's sections, then request only the ones needed. **Does not require approval**, read-only operation. ### Parameters | Parameter | Type | Required | Default | Description | |-----------|------|:--------:|---------|-------------| | `name` | string | Yes | n/a | Name of the skill to load content from (must match a loaded skill's name) | | `sections` | array | No | all sections | Only return sections matching these heading paths (case-insensitive partial match, e.g. `["Messaging", "Error Handling"]`) | | `max_tokens` | number | No | unbounded | Maximum total tokens to return, content is truncated once the budget is hit | | `include_preamble` | boolean | No | `true` | Whether to include the intro content before the first heading | | `max_depth` | number | No | unbounded | Depth limit for nested section inclusion (`1` = top-level only) | ### Response ```typescript // Success { success: true, name: 'slack', content: '## Messaging\n\n...\n\n## Error Handling\n\n...', tokensUsed: 550, // Approximate token count of the returned content message: 'Retrieved content for skill "slack" (550 tokens).' } // Skill not found { success: false, name: 'nonexistent', message: 'Skill "nonexistent" not found' } ``` ### Example ```typescript const content = await matimo.execute('matimo_get_skill_content', { name: 'slack', sections: ['Messaging'], max_tokens: 500, }); ``` ### Internal Flow 1. Requires an active global Matimo instance, same requirement as `matimo_search_skills`. 2. Delegates to `MatimoInstance.getSkillContent()` / `Matimo.get_skill_content()`, passing through `sections`/`max_tokens`/`include_preamble`/`max_depth` as `SkillContentOptions`. 3. When `max_tokens` is set, the underlying section-tree walk budget-checks **each individual section node** (pre-order), not the whole rendered subtree at once, a skill can be truncated to a partial prefix rather than returning empty content the moment any single top-level section exceeds the budget. 4. Returns `null` from the underlying method (surfaced here as `success: false`) if no loaded skill matches `name`. 5. `tokensUsed` is computed locally in the tool wrapper via a words÷0.75 heuristic, the same estimate used for `matimo_get_skill_sections`' `tokenEstimate` field. --- ## Usage Across Interfaces All meta-tools work consistently across SDK (TypeScript or Python), LangChain, and MCP: ### TypeScript SDK ```typescript const result = await matimo.execute('matimo_validate_tool', { yaml_content: '...' }); ``` ### Python SDK ```python from matimo import Matimo, PolicyContext, set_global_matimo_instance # matimo_create_tool, matimo_approve_tool and matimo_reload_tools need approval. matimo = await Matimo.init('./tools', auto_discover=True, on_approval=ask_reviewer) set_global_matimo_instance(matimo) # optional: a meta-tool run through matimo.execute() already acts on that instance; # register it for the @tool decorator or calls made outside execute() # Validate a tool definition result = await matimo.execute('matimo_validate_tool', {'yaml_content': '...'}) print(result['valid'], result['riskLevel']) # Full lifecycle from Python validate = await matimo.execute('matimo_validate_tool', {'yaml_content': yaml_str}) create = await matimo.execute('matimo_create_tool', {'name': 'my_tool', 'yaml_content': yaml_str, 'target_dir': './agent-tools'}) approve = await matimo.execute('matimo_approve_tool', {'name': 'my_tool', 'tool_dir': './agent-tools'}, context=PolicyContext(agent_id='reviewer', roles=['admin'])) reload = await matimo.execute('matimo_reload_tools', {}) # './agent-tools' must be in the tool paths status = await matimo.execute('matimo_get_tool_status',{'name': 'my_tool', 'tool_dir': './agent-tools'}) ``` > See [`python/examples/native/meta_flow/meta_tools_integration.py`](https://github.com/tallclub/matimo/blob/main/python/examples/native/meta_flow/meta_tools_integration.py) for an end-to-end Python demo of the tool-creation lifecycle (`matimo_validate_tool` → `matimo_create_tool` → `matimo_reload_tools` → `matimo_approve_tool` → `matimo_reload_tools` → `matimo_list_user_tools`). > Run it with: `cd python && make meta-flow` ### LangChain The LLM decides which meta-tool to call based on the conversation: ``` User: "Create a weather lookup tool" Agent → matimo_validate_tool (check YAML) Agent → matimo_create_tool (write the draft; on_approval asks) Agent → matimo_approve_tool (a human decides; on_approval asks) Agent → matimo_reload_tools (load into registry; on_approval asks) Agent → weather_lookup (use the new tool; on_approval asks each call) ``` ### MCP ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "matimo_validate_tool", "arguments": { "yaml_content": "name: my_tool\n..." } } } ``` For tools with `requires_approval: true`, the server asks the client's user through MCP elicitation. Clients that confirm calls with their user themselves can instead send `_matimo_approved: true`, which only counts when the server was started with `trustClientApproval: true`: ```json { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "matimo_create_tool", "arguments": { "name": "my_tool", "yaml_content": "...", "_matimo_approved": true } } } ``` --- ## File Locations All meta-tools are located in `typescript/packages/core/tools/` (Python mirror: `python/packages/core/src/matimo/tools/`): ``` typescript/packages/core/tools/ matimo_validate_tool/ definition.yaml matimo_validate_tool.ts matimo_create_tool/ definition.yaml matimo_create_tool.ts matimo_approve_tool/ definition.yaml matimo_approve_tool.ts matimo_reload_tools/ definition.yaml matimo_reload_tools.ts matimo_list_user_tools/ definition.yaml matimo_list_user_tools.ts matimo_get_tool_status/ definition.yaml matimo_get_tool_status.ts matimo_get_tool/ definition.yaml matimo_get_tool.ts matimo_search_tools/ definition.yaml matimo_search_tools.ts matimo_create_skill/ definition.yaml matimo_create_skill.ts matimo_list_skills/ definition.yaml matimo_list_skills.ts matimo_get_skill/ definition.yaml matimo_get_skill.ts matimo_validate_skill/ definition.yaml matimo_validate_skill.ts matimo_search_skills/ definition.yaml matimo_search_skills.ts matimo_get_skill_sections/ definition.yaml matimo_get_skill_sections.ts matimo_get_skill_content/ definition.yaml matimo_get_skill_content.ts shared/ skill-validation.ts ``` --- ## See Also - [Policy & Lifecycle Guide](https://matimo.dev/docs/api-reference/policy-and-lifecycle.md): Complete policy engine and lifecycle documentation - [Tool Specification](https://matimo.dev/docs/tool-development/tool-specification.md): YAML tool definition format reference - [Adding Tools](https://matimo.dev/docs/tool-development/adding-tools.md): How to add new tool providers - [Approval System](https://matimo.dev/docs/api-reference/approval-system.md): Approval handler configuration --- # Matimo MCP Server Source: https://matimo.dev/docs/mcp-server/ Expose your Matimo tools to AI assistants via the [Model Context Protocol](https://modelcontextprotocol.io/). One command gives Claude Desktop, Cursor, Windsurf, or any MCP client access to Slack, GitHub, Gmail, Notion, HubSpot, and more. --- ## Table of Contents - [SDK Selection: TypeScript vs Python](#sdk-selection-typescript-vs-python) - [Quick Start (5 minutes)](#quick-start-5-minutes) - [TypeScript](#typescript) - [Python](#python) - [Step-by-Step Setup](#step-by-step-setup) - [Client Configuration](#client-configuration) - [Claude Desktop](#claude-desktop) - [Cursor](#cursor) - [Windsurf](#windsurf) - [HTTP Mode (Remote / Docker)](#http-mode-remote--docker) - [Available Tool Packages](#available-tool-packages) - [CLI Reference](#cli-reference) - [HTTP Mode](#http-mode) - [Bearer Token Authentication](#bearer-token-authentication) - [HTTPS](#https) - [Docker](#docker) - [Secret Management](#secret-management) - [Tool Filtering](#tool-filtering) - [Approval over MCP](#approval-over-mcp) - [Tool Metadata & Error Responses](#tool-metadata--error-responses) - [Programmatic Usage](#programmatic-usage) - [Architecture](#architecture) - [Troubleshooting](#troubleshooting) - [TypeScript](#troubleshooting) - [Python](#troubleshooting-python) --- ## SDK Selection: TypeScript vs Python **Both TypeScript and Python SDKs serve MCP; see known differences in the release notes.** | Feature | TypeScript | Python | |---------|-----------|--------| | Core MCP server | Yes | Yes | | Stdio transport (Claude Desktop) | Yes | Yes | | HTTP transport (remote / Docker) | Yes | Yes | | Auth parameter filtering | Yes | Yes | | Approval via MCP elicitation (`_matimo_approved` only with `trustClientApproval`) | Yes | Yes | | `context` option, policy context (agent id, roles) for every MCP call | Yes | Yes | | Pre-resolved secrets (memory storage) | Yes | Yes | | Skill resources (MCP resources/list) | Yes | Yes | | Bearer token auth (HTTP) | Yes | Yes | | Tool filtering (allow/deny lists) | Yes | Yes | | Test coverage | 95%+ | 95%+ | ### Which SDK? - **TypeScript:** Use if you're in a Node.js environment or need the fastest startup - **Python:** Use if you prefer Python, need async/await in your agent, or want CrewAI / standard Python asyncio integration Both have identical capabilities and security features. Choose based on your language preference. **Implementation details:** - **TypeScript:** `typescript/packages/core/src/mcp/`, see [mcp-server.ts](https://github.com/tallclub/matimo/blob/main/typescript/packages/core/src/mcp/mcp-server.ts) - **Python:** `python/packages/core/src/matimo/mcp/`, see [README.md](https://github.com/tallclub/matimo/blob/main/python/packages/core/src/matimo/mcp/README.md) --- ## Quick Start (5 minutes) ### TypeScript ```bash mkdir my-ai-tools && cd my-ai-tools npm init -y ``` ### 2. Install Matimo + the tool packages you want ```bash npm install @matimo/core @matimo/cli @matimo/slack @matimo/github ``` ### 3. Set your API keys ```bash export SLACK_BOT_TOKEN=xoxb-your-slack-token export GITHUB_TOKEN=ghp_your-github-token ``` ### 4. Run the setup wizard ```bash npx matimo mcp setup ``` This scans your project, lists all discovered tools, shows which API keys are set () or missing (), and outputs **ready-to-paste configs** for Claude Desktop, Cursor, and HTTP mode. ### 5. Start the server ```bash npx matimo mcp ``` That's it. All installed `@matimo/*` tools are auto-discovered and exposed over stdio. ### Python 1. **Create a new project (or use an existing one)** ```bash mkdir my-ai-tools && cd my-ai-tools python -m venv .venv source .venv/bin/activate # or .venv\Scripts\activate on Windows ``` 2. **Install Matimo + the tool packages you want** ```bash pip install matimo matimo[mcp] # Add specific tool packages (optional) pip install matimo-slack matimo-github matimo-gmail matimo-notion ``` 3. **Set your API keys** ```bash export SLACK_BOT_TOKEN=xoxb-your-slack-token export GITHUB_TOKEN=ghp_your-github-token ``` 4. **Start the server** ```python # mcp_server.py import asyncio from matimo import Matimo from matimo.mcp.server import MCPServer, MCPServerOptions async def main(): matimo = await Matimo.init( auto_discover=True, # Discover installed tool packages ) server = MCPServer( matimo, MCPServerOptions(transport="stdio") ) await server.start() if __name__ == "__main__": asyncio.run(main()) ``` Then run: ```bash python mcp_server.py ``` That's it. All installed `matimo-*` tools are auto-discovered and exposed over stdio. --- ## Step-by-Step Setup ### Prerequisites - **Node.js 22.19+**: [Download](https://nodejs.org/) - **npm or pnpm**: comes with Node.js ### Install packages Pick the tool packages you need: ```bash # Core + CLI (always required) npm install @matimo/core @matimo/cli # Add the tools you want npm install @matimo/slack # Slack messaging, channels, reactions npm install @matimo/github # GitHub repos, issues, PRs npm install @matimo/gmail # Gmail send, read, drafts npm install @matimo/notion # Notion pages, databases npm install @matimo/hubspot # HubSpot CRM, contacts, deals npm install @matimo/postgres # PostgreSQL queries npm install @matimo/twilio # SMS, MMS messaging npm install @matimo/mailchimp # Email campaigns, audiences ``` Or install everything at once: ```bash npm install @matimo/core @matimo/cli @matimo/slack @matimo/github @matimo/gmail @matimo/notion @matimo/hubspot @matimo/postgres @matimo/twilio @matimo/mailchimp ``` ### Set up API keys Each tool package requires its own API credentials. Set them as environment variables: ```bash # Slack export SLACK_BOT_TOKEN=xoxb-your-token # GitHub export GITHUB_TOKEN=ghp_your-token # Gmail export GMAIL_ACCESS_TOKEN=ya29.your-token # Notion export NOTION_API_KEY=ntn_your-key # HubSpot export MATIMO_HUBSPOT_API_KEY=pat-your-key # PostgreSQL: a connection string (recommended) ... export MATIMO_POSTGRES_URL=postgresql://your-user:your-password@localhost:5432/your-database # ... or the separate variables # export MATIMO_POSTGRES_HOST=localhost # export MATIMO_POSTGRES_PORT=5432 # export MATIMO_POSTGRES_USER=your-user # export MATIMO_POSTGRES_PASSWORD=your-password # export MATIMO_POSTGRES_DB=your-database # Twilio (the sender number is the tool's "from" argument, not an environment variable) export TWILIO_ACCOUNT_SID=AC... export TWILIO_AUTH_TOKEN=your-auth-token # Mailchimp export MAILCHIMP_API_KEY=your-key-us1 ``` > **Tip:** Use a `.env` file instead of exporting each variable. See [Secret Management](#secret-management). ### Verify your setup ```bash npx matimo mcp setup ``` Sample output: ``` Matimo MCP Setup Scanning for installed tool packages... Found 22 tools across 2 package(s): slack (16 tools) • slack_send_channel_message • slack-list-channels • slack_get_channel_history • slack_search_messages • slack_send_dm ... and 11 more gmail (6 tools) • gmail-send-email • gmail-list-messages • gmail-get-message • gmail-create-draft • gmail-delete-message ... and 1 more Required environment variables: SLACK_BOT_TOKEN GMAIL_ACCESS_TOKEN Claude Desktop config (paste into Settings → Developer → MCP Servers): { "mcpServers": { "matimo": { "command": "npx", "args": ["matimo", "mcp"], "env": { "SLACK_BOT_TOKEN": "xoxb-your-token", "GMAIL_ACCESS_TOKEN": "" } } } } ``` --- ## Client Configuration ### Claude Desktop 1. Open Claude Desktop 2. Go to **Settings → Developer → Edit Config** 3. Paste the config from `npx matimo mcp setup`, or use this template: ```json { "mcpServers": { "matimo": { "command": "npx", "args": ["matimo", "mcp"], "env": { "SLACK_BOT_TOKEN": "xoxb-your-token", "GITHUB_TOKEN": "ghp_your-token" } } } } ``` 4. Restart Claude Desktop 5. Look for the tools icon in the chat, your Matimo tools should appear > **Important:** Claude Desktop can start commands from the root directory (`/`), not your project folder. Matimo looks for `@matimo/*` packages in the `node_modules` above the working directory first, then above where `@matimo/core` itself is installed, so install `@matimo/cli` and the tool packages in the same project and it will still find them. If tools still aren't detected, point at your own tool folders with `"args": ["matimo", "mcp", "--tool-paths", "/path/to/your/tools"]`. There is no `MATIMO_CWD` setting. ### Cursor Create `.cursor/mcp.json` in your project root: ```json { "mcpServers": { "matimo": { "command": "npx", "args": ["matimo", "mcp"], "env": { "SLACK_BOT_TOKEN": "xoxb-your-token", "GITHUB_TOKEN": "ghp_your-token" } } } } ``` ### Windsurf Create `.windsurf/mcp.json` in your project root: ```json { "mcpServers": { "matimo": { "command": "npx", "args": ["matimo", "mcp"], "env": { "SLACK_BOT_TOKEN": "xoxb-your-token", "GITHUB_TOKEN": "ghp_your-token" } } } } ``` ### Claude Desktop (Python) 1. Create a Python script to run the MCP server (e.g., `mcp_server.py`) 2. Open Claude Desktop 3. Go to **Settings → Developer → Edit Config** 4. Paste this config: ```json { "mcpServers": { "matimo": { "command": "python", "args": ["/path/to/mcp_server.py"], "env": { "SLACK_BOT_TOKEN": "xoxb-your-token", "GITHUB_TOKEN": "ghp_your-token" } } } } ``` 5. Restart Claude Desktop 6. Look for the tools icon, your Matimo tools should appear ### Cursor (Python) Create `.cursor/mcp.json` in your project root: ```json { "mcpServers": { "matimo": { "command": "python", "args": ["mcp_server.py"], "env": { "SLACK_BOT_TOKEN": "xoxb-your-token", "GITHUB_TOKEN": "ghp_your-token" } } } } ``` ### Windsurf (Python) Create `.windsurf/mcp.json` in your project root: ```json { "mcpServers": { "matimo": { "command": "python", "args": ["mcp_server.py"], "env": { "SLACK_BOT_TOKEN": "xoxb-your-token", "GITHUB_TOKEN": "ghp_your-token" } } } } ``` ### HTTP Mode (Remote / Docker) For remote servers, Docker containers, or MCP clients that connect over HTTP/HTTPS: ```bash npx matimo mcp --transport http --port 3000 ``` The server auto-generates a bearer token on startup: ``` Matimo MCP server running at http://localhost:3000/mcp Bearer Token (auto-generated): 550e8400-e29b-41d4-a716-446655440000 Connect your MCP client: url: http://localhost:3000/mcp Authorization: Bearer 550e8400-e29b-41d4-a716-446655440000 To use a fixed token, set MATIMO_MCP_TOKEN or use --token ``` Configure your MCP client with the displayed URL and token. For clients that support remote MCP: ```json { "mcpServers": { "matimo": { "url": "http://your-server:3000/mcp", "headers": { "Authorization": "Bearer 550e8400-e29b-41d4-a716-446655440000" } } } } ``` #### Python HTTP Mode Start the server: ```python # mcp_server_http.py import asyncio from matimo import Matimo from matimo.mcp.server import MCPServer, MCPServerOptions async def main(): matimo = await Matimo.init(auto_discover=True) server = MCPServer( matimo, MCPServerOptions( transport="http", port=3000, mcp_token="your-secret-token" # or use MATIMO_MCP_TOKEN env var ) ) await server.start() if __name__ == "__main__": asyncio.run(main()) ``` Then run: ```bash python mcp_server_http.py ``` Configure your MCP client: ```json { "mcpServers": { "matimo": { "url": "http://localhost:3000/mcp", "headers": { "Authorization": "Bearer your-secret-token" } } } } ``` --- ## Available Tool Packages | Package | Tools | Required Env Vars | |---------|-------|-------------------| | `@matimo/slack` | Send messages, list channels, search, reactions, threads, file uploads, DMs, create channels, set topics | `SLACK_BOT_TOKEN` | | `@matimo/github` | Repository management, issues, pull requests, code search | `GITHUB_TOKEN` | | `@matimo/gmail` | Send email, list messages, read messages, create drafts, delete | `GMAIL_ACCESS_TOKEN` | | `@matimo/notion` | Pages, databases, blocks, search | `NOTION_API_KEY` | | `@matimo/hubspot` | Contacts, companies, deals, tickets, engagements | `MATIMO_HUBSPOT_API_KEY` | | `@matimo/postgres` | Execute SQL queries (read + write) | `MATIMO_POSTGRES_URL`, or `MATIMO_POSTGRES_HOST`, `_PORT`, `_USER`, `_PASSWORD`, `_DB` | | `@matimo/twilio` | Send SMS/MMS, manage messages | `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN` | | `@matimo/mailchimp` | Audiences, subscribers, campaigns | `MAILCHIMP_API_KEY` | | `@matimo/microsoft` | Graph search, files, mail, Teams, calendar, SharePoint | `MICROSOFT_GRAPH_ACCESS_TOKEN` | | `@matimo/bruno` | Create, import and run Bruno API collections | none, but the `bru` CLI must be on PATH (`npm install -g @usebruno/cli`) | | `@matimo/composio` | 449 governed Composio tools across 14 toolkits | `COMPOSIO_API_KEY`, plus a Composio user or connected account per call | ### Auto-Discovery Mechanism Both TypeScript and Python SDKs discover tools identically via **entry points + filesystem scan**: #### How It Works When you call `MatimoInstance.init(autoDiscover: true)` or `Matimo.init(auto_discover=True)`: 1. **Entry Points Discovery**: Scans installed `@matimo/*` (TypeScript) or `matimo-*` (Python) packages 2. **Filesystem Scan**: Recursively scans `toolPaths` for tool definitions (YAML or JavaScript/Python files) 3. **Registry**: Registers all discovered tools so they're available over MCP #### Auto-Discovered Tool Inventory | Provider | Tool Count | Status | |----------|-----------|--------| | **Core** (9 built-in + 15 meta-tools) | 24 | Included by default | | `@matimo/slack` (or `matimo-slack`) | 16 | Requires `@matimo/slack` package + `SLACK_BOT_TOKEN` | | `@matimo/github` (or `matimo-github`) | 22 | Requires `@matimo/github` package + `GITHUB_TOKEN` | | `@matimo/gmail` (or `matimo-gmail`) | 6 | Requires `@matimo/gmail` package + `GMAIL_ACCESS_TOKEN` | | `@matimo/notion` (or `matimo-notion`) | 7 | Requires `@matimo/notion` package + `NOTION_API_KEY` | | `@matimo/postgres` (or `matimo-postgres`) | 1 | Requires `@matimo/postgres` package + `MATIMO_POSTGRES_URL` or `MATIMO_POSTGRES_*` | | `@matimo/twilio` (or `matimo-twilio`) | 4 | Requires `@matimo/twilio` package + `TWILIO_ACCOUNT_SID` and `TWILIO_AUTH_TOKEN` | | `@matimo/hubspot` (or `matimo-hubspot`) | 50 | Requires `@matimo/hubspot` package + `MATIMO_HUBSPOT_API_KEY` | | `@matimo/mailchimp` (or `matimo-mailchimp`) | 7 | Requires `@matimo/mailchimp` package + `MAILCHIMP_API_KEY` | | `@matimo/microsoft` (or `matimo-microsoft`) | 9 | Requires `@matimo/microsoft` package + `MICROSOFT_GRAPH_ACCESS_TOKEN` | | `@matimo/bruno` (or `matimo-bruno`) | 7 | Requires `@matimo/bruno` package and the `bru` CLI on PATH | | **Total (all native providers installed)** | **153** | 129 provider tools + 24 core | | **Total (default, core only)** | **24** | n/a | `@matimo/composio` adds another 449 tools when installed. That is too many to expose to most clients, so use `--tools` to pick a subset. #### TypeScript Example ```typescript import { MatimoInstance } from '@matimo/core'; // Auto-discover all installed @matimo/* packages const matimo = await MatimoInstance.init({ autoDiscover: true, // Optional: add custom tool directories toolPaths: ['./my-tools', '/path/to/other-tools'], }); // Print what was discovered const tools = matimo.listTools(); console.log(`✓ Discovered ${tools.length} tools`); // Start the MCP server const server = new MCPServer(matimo, { transport: 'stdio' }); await server.start(); ``` #### Python Example ```python import asyncio from matimo import Matimo from matimo.mcp.server import MCPServer, MCPServerOptions async def main(): # Auto-discover all installed matimo-* packages matimo = await Matimo.init( auto_discover=True, # Optional: add custom tool directories tool_paths=['./my-tools', '/path/to/other-tools'], ) # Print what was discovered tools = matimo.list_tools() print(f'✓ Discovered {len(tools)} tools') # Start the MCP server server = MCPServer(matimo, MCPServerOptions(transport='stdio')) await server.start() if __name__ == "__main__": asyncio.run(main()) ``` #### Tools discovered per package Both SDKs discover the same tool definitions from each installed package (counted from the YAML definitions shipped in 0.2.0): | Package | TypeScript | Python | |---------|-----------:|-------:| | core (built-in tools and meta-tools) | 24 | 24 | | slack | 16 | 16 | | github | 22 | 22 | | gmail | 6 | 6 | | notion | 7 | 7 | | postgres | 1 | 1 | | twilio | 4 | 4 | | hubspot | 50 | 50 | | mailchimp | 7 | 7 | | microsoft | 9 | 9 | The server lists only the packages installed in its environment, plus any `toolPaths` you pass. --- ## CLI Reference ``` matimo mcp [options] Start the MCP server matimo mcp setup Run the setup wizard ``` ### Options | Flag | Short | Default | Description | |------|-------|---------|-------------| | `--transport` | `-t` | `stdio` | Transport mode: `stdio` or `http` | | `--port` | `-p` | `3000` | HTTP port (only with `--transport http`) | | `--host` | | `127.0.0.1` | Interface to bind (only with `--transport http`). Use `0.0.0.0` in containers or for remote clients, and then use HTTPS | | `--tools` | | all | Comma-separated allowlist of tool names | | `--exclude` | | none | Comma-separated denylist of tool names | | `--token` | | auto | Bearer token for HTTP mode. Auto-generated if not set. Also reads `MATIMO_MCP_TOKEN` env var. | | `--https` | | off | Enable HTTPS | | `--self-signed` | | off | Auto-generate a self-signed TLS cert (implies `--https`) | | `--cert` | | | Path to TLS certificate PEM file (implies `--https`) | | `--key` | | | Path to TLS private key PEM file (implies `--https`) | | `--secrets` | | `env` | Secret resolver chain: `env`, `dotenv`, `vault`, `aws` | | `--env-file` | | `.env` | Path to .env file (with `--secrets dotenv`) | | `--vault-path` | | `secret/data/matimo` | Vault KV v2 path (with `--secrets vault`) | | `--aws-secret-id` | | `matimo/credentials` | AWS Secrets Manager ID (with `--secrets aws`) | | `--tool-paths` | | auto | Comma-separated paths to tool directories | | `--skill-paths` | | auto | Comma-separated paths to skill directories | ### Examples ```bash # Default: stdio mode for Claude Desktop / Cursor npx matimo mcp # HTTP mode on custom port npx matimo mcp --transport http --port 8080 # Only expose specific tools npx matimo mcp --tools slack_send_channel_message,slack-list-channels # Expose everything except Postgres tools npx matimo mcp --exclude postgres-execute-sql # HTTP with explicit bearer token npx matimo mcp --transport http --token my-secret-token # HTTPS with auto-generated self-signed certificate npx matimo mcp --transport http --self-signed # HTTPS with your own certificates npx matimo mcp --transport http --cert /path/to/cert.pem --key /path/to/key.pem # HTTPS + custom port + fixed token npx matimo mcp --transport http --port 8443 --self-signed --token my-secret # Use .env file for secrets npx matimo mcp --secrets dotenv --env-file ./config/.env # Chain resolvers: try env vars first, then Vault npx matimo mcp --secrets env,vault --vault-path secret/data/matimo ``` --- ## HTTP Mode HTTP mode runs a stateful HTTP server for remote access, Docker containers, or MCP clients that support HTTP transport. ### Endpoints | Method | Path | Auth Required | Description | |--------|------|---------------|-------------| | `POST` | `/mcp` (or `/`) | Yes | Start a new MCP session (JSON-RPC initialize) | | `GET` | `/mcp` (or `/`) | Yes | Open SSE stream for an existing session (`Mcp-Session-Id` header required) | | `DELETE` | `/mcp` (or `/`) | Yes | Close an existing session (`Mcp-Session-Id` header required) | | `GET` | `/health` | No | Health check, returns `{"status":"ok","tools":}` | | `OPTIONS` | `*` | No | CORS preflight | ### Bearer Token Authentication HTTP mode **always** requires a bearer token. The server handles this automatically: | Priority | Source | Example | |----------|--------|---------| | 1st | `--token` flag | `--token my-secret` | | 2nd | `MATIMO_MCP_TOKEN` env var | `export MATIMO_MCP_TOKEN=my-secret` | | 3rd | Auto-generated | UUID printed to console on startup | Clients must include `Authorization: Bearer ` in every request (except `/health`). ```bash # Test health (no auth needed) curl http://localhost:3000/health # Test auth: initialize a new session curl -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-11-25" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"test","version":"0.1.0"}}}' \ http://localhost:3000/mcp ``` > **Note:** The [MCP spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#protocol-version-header) > requires clients to include an `MCP-Protocol-Version` header on all requests after initialization. > The server currently accepts requests without this header for backwards compatibility. > **Tip:** For production, set a fixed token with `MATIMO_MCP_TOKEN` or `--token` so it survives server restarts. ### MCP Spec Compliance Notes Matimo HTTP mode is built on the official `@modelcontextprotocol/sdk` and implements the [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http) specification. The following areas are fully compliant: - Stateful sessions via `Mcp-Session-Id` (SDK-managed) - Per-session `McpServer` instance (not shared across clients) - Bearer token authentication - `POST /mcp` for JSON-RPC, `GET /mcp` for SSE, `DELETE /mcp` for session close - `isError: true` in tool failure responses - stdio mode: no non-JSON-RPC output on stdout (logger silenced) Known gaps against the spec (tracked for a future release): | Gap | Spec requirement | Impact | |-----|-----------------|--------| | `Origin` header not validated | [Spec §2.0.1](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#security-warning): servers **MUST** validate `Origin` to prevent DNS rebinding | Local-only servers using `--self-signed` are lower risk; production servers behind a reverse proxy should add `Origin` validation at the proxy layer | | `MCP-Protocol-Version` header not enforced | [Spec §2.7](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#protocol-version-header): invalid version must return `400` | No impact with current MCP SDK clients; all known clients send a valid version | ### HTTPS Enable HTTPS for encrypted connections. Two options: #### Self-signed certificate (development) ```bash npx matimo mcp --transport http --self-signed ``` - Generates an RSA 2048 key + X.509 certificate via `openssl` (key via Node.js `crypto`) - SAN covers `DNS:localhost` and `IP:127.0.0.1`, valid for 365 days - Cached in `.matimo/certs/`, reused across restarts - Because the cert is self-signed, clients won't trust it by default, see [HTTPS client can't connect (self-signed cert)](#https-client-cant-connect-self-signed-cert) for options ranked by security Output: ``` Matimo MCP server running at https://localhost:3000/mcp HTTPS enabled (self-signed certificate) Clients may need to disable cert verification for self-signed certs Bearer Token (auto-generated): 07b9b426-5f26-4149-9f12-5f83224ceffe ``` #### Production certificates ```bash npx matimo mcp --transport http --cert /etc/ssl/cert.pem --key /etc/ssl/key.pem ``` Use certificates from Let's Encrypt, your cloud provider, or your internal CA. ### Docker ```dockerfile FROM node:22-alpine # openssl needed for self-signed cert generation RUN apk add --no-cache openssl # Install Matimo globally RUN npm install -g @matimo/core @matimo/cli @matimo/slack @matimo/github EXPOSE 3000 CMD ["matimo", "mcp", "--transport", "http", "--host", "0.0.0.0", "--port", "3000", "--self-signed"] ``` ```bash # Build docker build -t matimo-mcp . # Run with your API keys docker run -p 3000:3000 \ -e SLACK_BOT_TOKEN=xoxb-your-token \ -e GITHUB_TOKEN=ghp_your-token \ -e MATIMO_MCP_TOKEN=my-secret \ matimo-mcp ``` For production, mount real certificates instead of using `--self-signed`: ```bash docker run -p 3000:3000 \ -v /etc/letsencrypt/live/mydomain:/certs:ro \ -e SLACK_BOT_TOKEN=xoxb-your-token \ -e MATIMO_MCP_TOKEN=my-secret \ matimo-mcp \ matimo mcp --transport http --cert /certs/fullchain.pem --key /certs/privkey.pem ``` --- ## Secret Management The MCP server needs API keys and tokens to call tool APIs. Matimo supports four secret resolver backends that can be chained. ### How It Works 1. On startup, the server scans all tool definitions for auth placeholders (e.g., `{SLACK_BOT_TOKEN}`) 2. The secret resolver chain resolves each placeholder, **first resolver to return a value wins** 3. Resolved secrets are seeded into `process.env` so tool execution works seamlessly ### Resolver: `env` (Default) Reads from environment variables. Checks `MATIMO_` first, then ``. ```bash export SLACK_BOT_TOKEN=xoxb-your-token npx matimo mcp ``` ### Resolver: `dotenv` Reads from a `.env` file. Great for development. ```ini # .env SLACK_BOT_TOKEN=xoxb-your-token GITHUB_TOKEN=ghp_your-token ``` ```bash npx matimo mcp --secrets dotenv npx matimo mcp --secrets dotenv --env-file ./config/.env ``` ### Resolver: `vault` Reads from HashiCorp Vault KV v2. Requires `node-vault` peer dependency. ```bash npm install node-vault export VAULT_ADDR=https://vault.example.com export VAULT_TOKEN=hvs.your-token npx matimo mcp --secrets vault --vault-path secret/data/matimo ``` Features: TTL-based caching (5 min default), stale fallback on connection errors, namespace support. ### Resolver: `aws` Reads from AWS Secrets Manager. Requires `@aws-sdk/client-secrets-manager` peer dependency. ```bash npm install @aws-sdk/client-secrets-manager # Store secrets as JSON in AWS Secrets Manager: # Secret name: matimo/credentials # Secret value: {"SLACK_BOT_TOKEN": "xoxb-...", "GITHUB_TOKEN": "ghp_..."} npx matimo mcp --secrets aws --aws-secret-id matimo/credentials ``` Features: TTL-based caching (5 min), uses standard AWS credential chain (env, IAM roles, SSO). ### Chaining Resolvers Resolvers are tried in order. First match wins. ```bash # Local dev: .env file first, then Vault as fallback npx matimo mcp --secrets dotenv,vault # Production: env vars first, then AWS npx matimo mcp --secrets env,aws ``` --- ## Tool Filtering ### Allowlist: only expose specific tools ```bash npx matimo mcp --tools slack_send_channel_message,slack-list-channels ``` ### Denylist: expose everything except certain tools ```bash npx matimo mcp --exclude postgres-execute-sql ``` ### Both: allowlist first, then denylist filters the result ```bash npx matimo mcp --tools slack_send_channel_message,slack_send_dm --exclude slack_send_dm ``` --- ## Approval over MCP An MCP server is usually driven by a model, so the model must never be able to approve its own calls. When a call needs approval, the tool declares `requires_approval: true`, it is an HTTP `DELETE` or a command tool (the 0.2.0 secure default), or a `sql`/`command` argument contains a destructive keyword ([details](https://matimo.dev/docs/api-reference/approval-system.md)), Matimo asks the **person using the client**: 1. The server sends an MCP **elicitation** request to the client: the tool name, its description and the arguments, with a single yes/no field. 2. The client shows it to its user, and the call runs only if they accept. 3. A client that does not support elicitation gets an error result saying there is no one to ask, never a hint that the model could approve the call itself. Options for clients without elicitation: | Option | Effect | |--------|--------| | `MATIMO_APPROVED_PATTERNS="get_*,list_*"` on the server | Matching tools never ask | | `trustClientApproval: true` (`trust_client_approval=True`) | Tools that need approval advertise an optional `_matimo_approved` argument, and a call with `_matimo_approved: true` counts as approved. Only for clients that confirm every call with their user themselves, the argument comes from the client and model. | `context` (`PolicyContext`) sets the identity and roles every call from this server is checked with, for example `{ agentId: 'claude-desktop', roles: ['admin'] }` on a single-user local server, so that user can approve agent-written tools with `matimo_approve_tool`. Without it, calls carry no roles. The model cannot set this context; it comes only from the server's options. See [Where roles come from](https://matimo.dev/docs/api-reference/policy-and-lifecycle.md#where-roles-come-from). `trustClientApproval` and `context` are programmatic options (below); the `matimo mcp` CLI does not set them. --- ## Tool Metadata & Error Responses ### Standard MCP annotations Every tool Matimo registers over MCP carries the protocol's standard [tool annotations](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#annotations), `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`, plus a humanized `title` (`slack_get_channel_history` → `"Slack Get Channel History"`). Clients that understand these hints (Claude Desktop, Cursor, etc.) can use them to decide when to prompt a user before invoking a tool, independent of Matimo's own policy gate. Annotations are derived from `execution.type` and HTTP method, not from the aggregate policy risk tier, the two can diverge (a GET and a DELETE tool can share a risk tier while having opposite `readOnlyHint`/`destructiveHint`): | Execution | `readOnlyHint` | `destructiveHint` | `idempotentHint` | `openWorldHint` | | --- | --- | --- | --- | --- | | `type: function` / `type: command` | `false` | `true` | `false` | `true` | | HTTP `GET` | `true` | `false` | `true` | `true` | | HTTP `PUT` | `false` | `false` | `true` | `true` | | HTTP `DELETE` | `false` | `true` | `true` | `true` | | HTTP `POST` / `PATCH` / other | `false` | `false` | `false` | `true` | A tool with `requires_approval: true` always gets `destructiveHint: true`, regardless of the table above. ### Structured error responses Failed tool calls return `isError: true` with a `structuredContent` field carrying machine-readable error data, instead of only a freeform error string: ```json { "content": [{ "type": "text", "text": "Error: Rate limit exceeded" }], "isError": true, "structuredContent": { "code": "RATE_LIMIT_EXCEEDED", "statusCode": 429, "retryable": true, "message": "Rate limit exceeded" } } ``` This applies uniformly to every error path: a thrown `MatimoError` (its `code`/`details.statusCode`/`details.retryable` map directly), any other exception (`code: "UNKNOWN_ERROR"`), and an approval-rejection response (`code: "EXECUTION_FAILED"`), so a client can always branch on `structuredContent.code` rather than parsing the text. See the [Error Codes Reference](https://matimo.dev/docs/api-reference/errors.md) for the full code list and what `retryable` means for each. --- ## Programmatic Usage Use the MCP server directly from TypeScript: ```typescript import { MCPServer, createMCPServer } from '@matimo/core'; // One-liner: start with defaults const server = await createMCPServer({ transport: 'stdio', autoDiscover: true, }); // Or with full control const server = new MCPServer({ transport: 'http', port: 3000, tools: ['slack_send_channel_message'], mcpToken: 'my-secret', https: true, selfSigned: true, secretResolver: { resolvers: [ { type: 'env' }, { type: 'vault', secretPath: 'secret/data/myapp' }, ], }, }); await server.start(); // Get the active token (useful when auto-generated) const token = server.getActiveToken(); console.log(`Token: ${token}`); // Later... await server.stop(); ``` ### MCPServerOptions | Option | Type | Default | Description | |--------|------|---------|-------------| | `transport` | `'stdio' \| 'http'` | `'stdio'` | Transport mode | | `port` | `number` | `3000` | HTTP port | | `tools` | `string[]` | all | Tool name allowlist | | `excludeTools` | `string[]` | none | Tool name denylist | | `mcpToken` | `string` | auto-generated | Bearer token for HTTP mode | | `toolPaths` | `string[]` | auto | Explicit tool directory paths | | `autoDiscover` | `boolean` | `true` | Auto-discover `@matimo/*` packages | | `https` | `boolean` | `false` | Enable HTTPS | | `selfSigned` | `boolean` | `false` | Auto-generate self-signed certificate | | `certPath` | `string` | | Path to TLS certificate PEM | | `keyPath` | `string` | | Path to TLS private key PEM | | `secretResolver` | `SecretResolverChainConfig` | env-only | Secret resolver chain config | | `skillPaths` | `string[]` | none | Skill directories, exposed as `skills://` resources | | `policyConfig` | `PolicyConfig` | defaults | Policy for the server's instance | | `untrustedPaths` | `string[]` | none | Directories whose tools must pass the content rules | | `approvalSecret` / `approvalDir` | `string` | env / cwd | Where `matimo_approve_tool` approvals are signed and stored | | `trustClientApproval` | `boolean` | `false` | Accept `_matimo_approved: true` from the client ([Approval over MCP](#approval-over-mcp)) | | `context` | `PolicyContext` | none | Identity and roles applied to every call | Python's `MCPServerOptions` has the same options in snake_case. Two defaults differ: `port` is `3100` and `auto_discover` is `False` in Python. --- ## Architecture ![MCP server startup: a client connects over stdio or HTTP, then the server resolves secrets, runs MatimoInstance.init with auto-discovery, filters tools, registers each tool on McpServer, and connects the transport.](../../../assets/diagrams/mcp-server-startup-light.svg) ![MCP server startup: a client connects over stdio or HTTP, then the server resolves secrets, runs MatimoInstance.init with auto-discovery, filters tools, registers each tool on McpServer, and connects the transport.](../../../assets/diagrams/mcp-server-startup-dark.svg) A tool call then flows like this: ![MCP tool call flow: the client sends tools/call, the MCPServer handler calls matimo.execute, which injects auth, runs the executor, validates the response, applies the size guardrail and returns MCP content.](../../../assets/diagrams/mcp-tool-call-light.svg) ![MCP tool call flow: the client sends tools/call, the MCPServer handler calls matimo.execute, which injects auth, runs the executor, validates the response, applies the size guardrail and returns MCP content.](../../../assets/diagrams/mcp-tool-call-dark.svg) ### Python Implementation Details The Python MCP implementation follows the TypeScript one; both SDKs serve MCP (see known differences in the release notes): #### Core Features | Component | TypeScript | Python | Details | |-----------|-----------|--------|---------| | Core server | `MCPServer` | `MCPServer` | Wraps Matimo instance, registers MCP handlers | | Auth filtering | `isAuthParameter()` | `_is_auth_parameter()` | Strips secrets from schemas | | Approval | `createElicitationApprovalCallback()` | `create_elicitation_approval_callback()` | Asks the client's user via elicitation; `_matimo_approved` only with `trustClientApproval` | | Secret resolution | `seedEnvironmentSecrets()` | `_seed_environment_secrets()` | Pre-resolves at startup, stores in memory | | Skill resources | `registerSkillResources()` | `_register_skill_resources()` | Registers skills as MCP resources | | HTTP transport | `StreamableHTTPServerTransport` | `StreamableHTTPSessionManager` | Stateless HTTP with bearer auth, CORS | | Stdio transport | `StdioServerTransport` | `stdio_server()` | JSON-RPC over pipe for Claude Desktop | | Tool annotations | `deriveToolAnnotations()` | `derive_tool_annotations()` | readOnlyHint/destructiveHint/idempotentHint/openWorldHint | | Structured errors | `structuredContent` in `registerTool()` catch block | `_build_error_result()` | code/statusCode/retryable/message on every error path, incl. a generic-exception catch-all | #### Auto-Discovery Implementation | Aspect | TypeScript | Python | Status | |--------|-----------|--------|--------| | Package discovery | Scans `node_modules/@matimo/*` | `importlib.metadata` entry points (`matimo.providers`) | Same result, different mechanism | | Filesystem traversal | `fs.readdirSync()` recursive scan | `os.walk()` recursive scan | Identical | | YAML loading | `js-yaml` + Zod validation | PyYAML + Pydantic validation | Identical | | Caching | In-memory Map | In-memory dict | Identical | | Tools loaded (core + all native providers) | 153 | 153 | Same set | | Discovery mechanism | Same | Same | Identical | **Key locations:** - **TypeScript:** `typescript/packages/core/src/core/tool-loader.ts` (entry points + filesystem) - **Python:** `python/packages/core/src/matimo/core/loader.py` (entry points + filesystem) **Language differences:** 1. **Package discovery:** TypeScript scans `node_modules/@matimo/*`, Python reads `importlib.metadata` entry points 2. **Filesystem scanning:** TypeScript uses `fs.*`, Python uses `os.*` 3. **YAML parsing:** Both use standard libraries with Zod (TS) / Pydantic (PY) validation 4. **Async/Await:** Python is async-first; TypeScript is top-level await #### Other Implementation Details **Key differences:** 1. **Language:** Python uses async/await throughout; TypeScript uses top-level await 2. **HTTP:** Python uses pure-ASGI with async context management; TypeScript uses Node HTTP + Node streams 3. **Secret storage:** Both store in memory **after resolution**, never written back to process env, Python uses `dict[str, str]`, TypeScript uses `Record` 4. **Tests:** Run `pnpm test` in `typescript/` or `make test` in `python/` For full implementation details, see [python/packages/core/src/matimo/mcp/README.md](https://github.com/tallclub/matimo/blob/main/python/packages/core/src/matimo/mcp/README.md). --- ## Troubleshooting ### TypeScript #### Tools not showing up in Claude Desktop 1. **Check your config path:** On macOS it's `~/Library/Application Support/Claude/claude_desktop_config.json` 2. **Restart Claude Desktop** after editing the config 3. **Install the tool packages next to `@matimo/cli`.** Claude Desktop can start the server from `/`; Matimo then finds packages relative to where it is installed. If your tools still aren't detected, pass their folder: `"args": ["matimo", "mcp", "--tool-paths", "/Users/you/your-project/tools"]` 4. **Run the setup wizard** to verify packages are installed: ```bash npx matimo mcp setup ``` #### No tools found / "No @matimo/* tool packages found" Make sure `@matimo/*` packages are installed in your project: ```bash npm ls | grep matimo # Should show @matimo/core, @matimo/cli, @matimo/slack, etc. ``` If empty, install them: ```bash npm install @matimo/core @matimo/cli @matimo/slack ``` #### "Tool requires approval" The call needs a person's approval and the client cannot ask one (it does not support MCP elicitation). Use a client that supports elicitation, pre-approve the tool on the server with `MATIMO_APPROVED_PATTERNS`, or, only if the client confirms every call with its user, start the server with `trustClientApproval: true`. See [Approval over MCP](#approval-over-mcp). #### Self-signed certificate fails to generate Requires `openssl` to be installed: ```bash # macOS (pre-installed) openssl version # Linux (Debian/Ubuntu) sudo apt install openssl # Alpine (Docker) apk add --no-cache openssl # Or skip self-signed and provide your own certs: npx matimo mcp --transport http --cert cert.pem --key key.pem ``` #### HTTPS client can't connect (self-signed cert) Self-signed certificates are not trusted by default. There are several ways to handle this, ordered from **most** to **least** secure. --- #### Option 1: Use a locally trusted certificate (recommended) [`mkcert`](https://github.com/FiloSottile/mkcert) creates certificates signed by a local CA that your OS and browser trust automatically. No bypass needed, no security warnings. ```bash # Install mkcert brew install mkcert # macOS sudo apt install mkcert # Linux (or build from source) # One-time: trust the local CA system-wide mkcert -install # Generate a cert for localhost mkcert localhost 127.0.0.1 ::1 # → produces localhost+2.pem and localhost+2-key.pem # Start Matimo MCP with the trusted cert npx matimo mcp --transport http --cert localhost+2.pem --key localhost+2-key.pem ``` All clients (curl, Node.js, browsers) on the same machine now connect without any special flags. --- #### Option 2: Use a CA-trusted certificate For staging or shared environments, use a certificate from a public CA (e.g. Let's Encrypt) or your organisation's internal CA. Pass it to the server: ```bash npx matimo mcp --transport http --cert cert.pem --key key.pem ``` --- #### Option 3: Narrow TLS bypass for development clients only If you can't use a trusted certificate, scope the bypass to **only** the MCP connection rather than the entire Node process. **curl**, the `-k` / `--insecure` flag applies to that request only: ```bash curl -k https://localhost:3000/health ``` **Node.js**, pass a custom `https.Agent` scoped to your MCP client. Never set `process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'`; it disables certificate validation globally for every HTTPS request in the process, including requests to OpenAI, Slack, or any other service. ```ts import https from 'https'; // Development only, never use rejectUnauthorized: false in production const devAgent = new https.Agent({ rejectUnauthorized: false }); const client = new MultiServerMCPClient({ mcpServers: { matimo: { transport: 'streamable_http', url: 'https://localhost:3000/mcp', // Pass the agent via fetch options if your MCP client supports it. }, }, }); ``` > **Security note:** Disabling certificate verification, even for a single connection, allows > an attacker on the same network to impersonate the server, intercept the bearer token, and > issue arbitrary tool calls. Only ever do this on a loopback interface (`localhost` / > `127.0.0.1`) in a controlled local development environment. **Never use it in production, > CI/CD pipelines, or shared environments.** #### Debug logging Enable verbose logging to see tool discovery, auth resolution, and execution details: ```bash MATIMO_LOG_LEVEL=debug npx matimo mcp ``` > **Note:** Debug logging is automatically silenced in stdio mode (stdout must be clean JSON-RPC). Logs go to stderr in stdio mode. #### Optional peer dependencies Vault and AWS secret resolvers require extra packages: ```bash npm install node-vault # For --secrets vault npm install @aws-sdk/client-secrets-manager # For --secrets aws ``` --- ## Troubleshooting (Python) ### "MCP Python SDK not installed" Error: `MatimoError(EXECUTION_FAILED): MCP Python SDK not installed. Install with: pip install matimo[mcp]` **Solution:** ```bash pip install "matimo[mcp]" # or explicitly pip install "mcp>=2.0" ``` The Python MCP server requires `mcp` 2.0 or newer. If you installed `mcp` 1.x, upgrade it with `pip install -U "mcp>=2.0"`. ### No tools discovered Error: Tools list is empty in Claude Desktop **Solution:** 1. Verify Matimo can find your tools: ```python from matimo import Matimo matimo = await Matimo.init(auto_discover=True) print(f"Found {len(matimo.list_tools())} tools") ``` 2. If empty, ensure tool packages are installed: ```bash pip list | grep matimo # Should show matimo, matimo-slack, matimo-github, etc. ``` 3. If missing, install them: ```bash pip install matimo matimo-slack matimo-github ``` ### "Stdio mode corrupt output / JSON-RPC decode error" Cause: Logging to stdout during stdio transport interferes with JSON-RPC **Solution:** Already fixed in the implementation, `start()` suppresses the matimo logger in stdio mode. If you see this error, check for custom log handlers writing to stdout: ```python # DON'T do this import logging logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) # DO this instead (if you need logging) logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) ``` ### HTTP 401 Unauthorized Error: `401 Unauthorized` when connecting to HTTP mode **Solution:** 1. Check the bearer token is set: ```bash export MATIMO_MCP_TOKEN="your-secret-token" python mcp_server.py ``` 2. Pass it in the MCP client config: ```json { "mcpServers": { "matimo": { "url": "http://localhost:3000/mcp", "headers": { "Authorization": "Bearer your-secret-token" } } } } ``` ### Secrets not resolved Error: `KeyError: 'SLACK_BOT_TOKEN'` during tool execution **Solution:** Ensure your `.env` file is readable and in the project root: ```bash # .env SLACK_BOT_TOKEN=xoxb-your-token GITHUB_TOKEN=ghp_your-token ``` Also verify Matimo is initialized with proper paths: ```python from matimo import Matimo matimo = await Matimo.init( ["./tools"], # ← tool paths auto_discover=True ) ``` ### asyncio event loop errors Error: `RuntimeError: no running event loop` or `RuntimeError: asyncio.run() called from a running event loop` **Solution:** Use `asyncio.run()` at the top level: ```python import asyncio async def main(): matimo = await Matimo.init(auto_discover=True) # ... # Start the event loop here if __name__ == "__main__": asyncio.run(main()) ``` Don't call `asyncio.run()` from inside async functions or Jupyter notebooks with existing loops. --- # Skills System Source: https://matimo.dev/docs/skills/skills/ > Complete guide to the Matimo Skills System, what skills are, how they are structured, how agents create and use them, and how they comply with the Agent Skills specification. ## Table of Contents - [When to Use Skills](#when-to-use-skills) - [Overview](#overview) - [Skills vs Tools](#skills-vs-tools) - [SKILL.md Format](#skillmd-format) - [Frontmatter Fields](#frontmatter-fields) - [Spec-Compliant Frontmatter](#spec-compliant-frontmatter) - [Progressive Disclosure Levels](#progressive-disclosure-levels) - [Minimal Valid Example](#minimal-valid-example) - [Provider Skills (One Per Provider)](#provider-skills-one-per-provider) - [Built-in SDK Skills](#built-in-sdk-skills) - [Content Chunking & Smart Context](#content-chunking--smart-context) - [getSkillContent(), Selective Loading](#getskillcontent-selective-loading) - [getSkillSections(), Section Inventory](#getskillsections-section-inventory) - [Skill Search (TF-IDF)](#skill-search-tf-idf) - [Built-in TF-IDF Search](#built-in-tf-idf-term-frequency---inverse-document-frequency-search) - [Custom Embedding Provider](#custom-embedding-provider) - [Agent Skill Lifecycle](#agent-skill-lifecycle) - [Creating a Skill](#creating-a-skill) - [Listing Skills](#listing-skills) - [Reading a Skill](#reading-a-skill) - [Validating a Skill](#validating-a-skill) - [Searching and Loading Skills Selectively](#searching-and-loading-skills-selectively) - [Pluggable Skill Storage, Bring Your Own Backend](#pluggable-skill-storage-bring-your-own-backend) - [MCP Server, Skills as Resources](#mcp-server-skills-as-resources) - [LangChain Agent with Skills](#langchain-agent-with-skills) - [CrewAI Agent with Skills](#crewai-agent-with-skills) - [Storage Paths](#storage-paths) - [Name Rules](#name-rules) - [Examples Demo](#examples-demo) --- ## When to Use Skills ### Decision Guide | Situation | Use Skills? | Why | |-----------|:-----------:|-----| | Agent needs domain knowledge at runtime (how to use Slack, Postgres patterns) | Yes | Pull knowledge on demand without bloating the system prompt | | Agent must execute an API call or shell command | No | Use a tool (YAML `definition.yaml`) | | Knowledge varies per user / per session | Yes | Agent reads the right skill for the task, not a static prompt | | You want the same guidelines reused by multiple agents | Yes | Write once in SKILL.md, any agent can discover and load it | | Knowledge is static and always relevant to every request | No | Put it in the system prompt instead | | You need structured, versioned, sharable agent expertise | Yes | Skills are files, version-controlled, composable, shareable | ### Real-World Use Cases **Use Case 1, Slack messaging agent** Without skills: System prompt contains 500 lines of Slack API guidelines (always in context, always costs tokens). With skills: Agent calls `matimo_get_skill("slack")` only when it needs to send a message, loads 500 lines on demand and discards them after. **Use Case 2, Multi-provider agent (Slack + GitHub + Postgres)** Agent serves many domains. At startup, list all skills (Level 1, ~50 tokens each). When the user asks about GitHub, agent reads only the `github` skill (Level 2). When user switches to Postgres, agent reads `postgres`. Total context = only the active skill, not all three simultaneously. **Use Case 3, Code review assistant** Agent creates a `code-review` skill at runtime (via `matimo_create_skill`) containing your team's specific checklist. Every code review session starts by reading that skill. Update the SKILL.md to immediately change agent behavior, no redeployment. **Use Case 4, Compliance-aware agent** A security team packages OWASP Top 10 rules as a `security-checklist` skill. Any agent in the org can discover and apply it. When rules update, update one file. **Use Case 5, Dynamic skill selection based on task** Agent receives a broad request: "Help me with infrastructure". It calls `matimo_list_skills`, sees `postgres` and `tool-creation` skills, picks the relevant one based on context, and loads only that, the LLM never sees the others. ### Benefits vs Alternatives | Approach | Context Cost | Reusability | Updateable at Runtime | Discoverable by Agent | |----------|:---:|:---:|:---:|:---:| | Static system prompt | Always full cost | No | Needs redeploy | No | | RAG / vector search | High infra cost | Partial | Yes | No | | **Matimo Skills** | Only when used | Yes | (edit SKILL.md) | Yes | --- ## Overview A **skill** is a structured knowledge file (`SKILL.md`) that teaches an agent *how to do something*, guidelines, patterns, checklists, and rules written in Markdown. Skills are distinct from tools: tools execute actions, skills provide context and guidance. Matimo supports the [Agent Skills specification](https://agentskills.io/specification) (`agentskills.io`) for SKILL.md formatting and validation. **Key idea:** Skills encode domain knowledge once. Agents can list, read, and apply skills at runtime, without re-reading long system prompts. ![An SDK developer writes SKILL.md files under packages/core/skills, and the agent loads, creates and validates skills at runtime with the matimo skill meta-tools.](../../../../assets/diagrams/skills-flow-light.svg) ![An SDK developer writes SKILL.md files under packages/core/skills, and the agent loads, creates and validates skills at runtime with the matimo skill meta-tools.](../../../../assets/diagrams/skills-flow-dark.svg) --- ### Skill vs Tool Decision Flowchart ![Decision tree: if it needs to run something, use a tool defined in definition.yaml; if it needs to teach the agent something, use a skill in SKILL.md; otherwise put it in the system prompt.](../../../../assets/diagrams/tool-or-skill-light.svg) ![Decision tree: if it needs to run something, use a tool defined in definition.yaml; if it needs to teach the agent something, use a skill in SKILL.md; otherwise put it in the system prompt.](../../../../assets/diagrams/tool-or-skill-dark.svg) --- ## Skills vs Tools | | **Skill** | **Tool** | |---|---|---| | **File** | `SKILL.md` | `definition.yaml` | | **Purpose** | Teach agent guidelines | Execute an action | | **Content** | Markdown prose + examples | YAML execution spec | | **Runtime effect** | Agent reads and applies | Agent calls → side effect | | **Policy gated?** | No | Yes | | **Approval required?** | Create: yes (meta-tool) | Create + approve: yes | | **SDK location** | `packages/core/skills/` | `packages/{provider}/tools/` | | **Agent location** | `./matimo-tools/skills/` | `./matimo-tools/` | --- ## SKILL.md Format Every skill is a Markdown file with YAML frontmatter at the top, stored as: ``` {skills_dir}/ {skill-name}/ SKILL.md ``` ### Frontmatter Fields The [Agent Skills spec](https://agentskills.io/specification) defines the following valid top-level frontmatter fields: | Field | Required | Description | |-------|:--------:|-------------| | `name` | Yes | Skill identifier (lowercase, hyphens, ≤64 chars) | | `description` | Yes | One or two sentences describing what the skill teaches | | `license` | No | SPDX identifier (e.g., `MIT`, `Apache-2.0`) | | `compatibility` | No | Compatible agent frameworks or versions | | `metadata` | No | Arbitrary key-value object for non-spec fields | | `allowed-tools` | No | List of tool names the skill may reference or invoke | ### Spec-Compliant Frontmatter > **Only six keys are valid at the top level.** Custom fields (`category`, `difficulty`, `applyTo`, `user-invokable`) must go under `metadata:`, placing them at top level violates the spec and will fail `matimo_validate_skill`. ```yaml --- name: code-review description: Guidelines for systematic code review focusing on correctness, security, and maintainability. license: MIT metadata: category: "Engineering" difficulty: "intermediate" apply-to: "review, pull-request" --- ``` **Wrong, non-spec fields at top level:** ```yaml --- name: code-review description: ... category: Engineering # not a spec field difficulty: intermediate # not a spec field --- ``` ### Progressive Disclosure Levels The spec defines three levels of detail an agent loads: | Level | Operation | What agent gets | |-------|-----------|-----------------| | **1** | `matimo_list_skills` | Name + description only | | **2** | `matimo_get_skill` | Full SKILL.md content | | **3** | Resources (scripts, assets) | Additional files alongside SKILL.md | Agents should use Level 1 first to survey available skills, then Level 2 to load the one they need, minimising context window usage. ### Minimal Valid Example ````markdown --- name: api-error-handling description: Patterns for handling API errors gracefully in TypeScript services. --- # API Error Handling ## When to Apply Apply when writing HTTP client code that calls external APIs. ## Required Patterns - Always check `response.ok` before parsing the body - Log the status code and URL on failure (never the response body if it may contain secrets) - Use `MatimoError` with appropriate `ErrorCode` for structured errors - Retry only on `429` and `5xx`, never on `4xx` ## Example ```typescript const response = await fetch(url); if (!response.ok) { throw new MatimoError( `API request failed: ${response.status}`, ErrorCode.EXECUTION_FAILED, { statusCode: response.status, url } ); } ``` ```` --- ## Provider Skills (One Per Provider) Every Matimo provider package ships **one consolidated skill** containing domain knowledge for all of that provider's tools. This keeps the skill registry compact and gives agents a single entry point per provider. | Provider | Skill Name | Path | Covers | |----------|-----------|------|--------| | **Slack** | `slack` | `packages/slack/skills/slack/SKILL.md` | 16 tools, channels, messages, reactions, threads, DMs, pins, bookmarks, reminders | | **GitHub** | `github` | `packages/github/skills/github/SKILL.md` | Issues, PRs, code search, repositories | | **Gmail** | `gmail` | `packages/gmail/skills/gmail/SKILL.md` | Email sending, inbox management, drafts, threads | | **HubSpot** | `hubspot` | `packages/hubspot/skills/hubspot/SKILL.md` | Contacts, companies, deals, CRM entities | | **Mailchimp** | `mailchimp` | `packages/mailchimp/skills/mailchimp/SKILL.md` | Audiences, subscribers, campaigns | | **Notion** | `notion` | `packages/notion/skills/notion/SKILL.md` | Pages, databases, blocks, search | | **Postgres** | `postgres` | `packages/postgres/skills/postgres/SKILL.md` | Parameterized queries, schema discovery, write operations | | **Twilio** | `twilio` | `packages/twilio/skills/twilio/SKILL.md` | SMS/MMS sending, message tracking, E.164 formatting | | **Microsoft** | `microsoft` | `packages/microsoft/skills/microsoft/SKILL.md` | 9 Graph API tools, search, OneDrive/SharePoint files, Outlook mail, Teams messaging, calendar, SharePoint publishing | | **Composio** | `composio` | `packages/composio/skills/composio/SKILL.md` | `composio_*` tools, how they route through Composio's 250+ third-party integrations, what risk levels mean for approval, handling missing connected accounts | **Why one skill per provider?** - Agents load one skill to get comprehensive knowledge for all tools in that provider - Reduces registry noise, 10 skills instead of dozens of individual tool skills - Content chunking lets agents load only the sections they need (see below) --- ## Built-in SDK Skills Matimo ships six SDK-level skills in `packages/core/skills/`. These are designed for agents and VS Code Copilot, not end-user runtimes. | Skill | Path | Purpose | |-------|------|---------| | `tool-creation` | `packages/core/skills/tool-creation/SKILL.md` | How to write correct YAML tool definitions, includes policy warnings, auth patterns, required fields | | `policy-validation` | `packages/core/skills/policy-validation/SKILL.md` | Security rules, blocked patterns (command/function types, SSRF), default policy values | | `meta-tools-lifecycle` | `packages/core/skills/meta-tools-lifecycle/SKILL.md` | The complete create → validate → approve → reload lifecycle using meta-tools | | `tool-discovery` | `packages/core/skills/tool-discovery/SKILL.md` | Tool listing, risk levels, status states, how to query the registry | | `skill-creator` | `packages/core/skills/skill-creator/SKILL.md` | How to create SKILL.md files following the Agent Skills spec | | `skills-catalog` | `packages/core/skills/skills-catalog/SKILL.md` | Catalog of all available skills and their purposes | ### Key Guidance in `tool-creation` Skill - **Never use placeholder API keys** (`YOUR_API_KEY`, `replace_me`), use `{VAR_NAME}` templating - **`command` and `function` types are blocked by default** (`allowCommandTools: false`, `allowFunctionTools: false`), always use `type: http` unless policy explicitly allows otherwise - **Agent-created tools always get `requires_approval: true` and `status: draft`**: `matimo_create_tool` sets both, whatever the YAML says - **Query-param API keys** need `query_params: { key: '{VAR_NAME}' }` + `authentication: { type: api_key, location: query }` --- ## Content Chunking & Smart Context Skills can be large (200+ lines). Dumping an entire SKILL.md into the LLM context wastes tokens. Matimo's content parser splits skills into sections automatically, and agents can load only what they need. ### getSkillContent(): Selective Loading Load specific sections of a skill by heading name: ```typescript // Load only "Error Handling" and "Parameterized Queries" sections const content = matimo.getSkillContent('postgres', { sections: ['Error Handling', 'Parameterized Queries'], maxTokens: 500, }); // Returns concatenated content from those sections only // Load everything (equivalent to getSkill().content) const full = matimo.getSkillContent('postgres'); ``` **`SkillContentOptions`:** ```typescript interface SkillContentOptions { /** Load only these section headings (case-insensitive substring match) */ sections?: string[]; /** Truncate output to approximately this many tokens (1 token ≈ 4 chars) */ maxTokens?: number; } ``` ### getSkillSections(): Section Inventory Before loading content, agents can inventory a skill's sections to decide what's relevant: ```typescript const sections = matimo.getSkillSections('slack'); // Returns: // [ // { path: 'Channel Operations', level: 2, tokenEstimate: 120 }, // { path: 'Messaging', level: 2, tokenEstimate: 200 }, // { path: 'Messaging > Thread Replies', level: 3, tokenEstimate: 80 }, // { path: 'Reactions & Pins', level: 2, tokenEstimate: 90 }, // ... // ] ``` This enables a two-step agent workflow: 1. `getSkillSections('slack')`: see what's available and token costs 2. `getSkillContent('slack', { sections: ['Messaging'] })`: load only what's needed Both are also agent-callable via the `matimo_get_skill_sections` and `matimo_get_skill_content` meta-tools, see [Searching and Loading Skills Selectively](#searching-and-loading-skills-selectively). --- ## Skill Search (TF-IDF) Matimo provides embedding-based search across all registered skills, so agents can find relevant knowledge by meaning rather than exact keywords. ### Built-in TF-IDF (Term Frequency - Inverse Document Frequency) Search The default provider uses zero-dependency TF-IDF embeddings with cosine similarity: ```typescript // Find skills related to a natural language query const results = await matimo.semanticSearchSkills('How do I handle Postgres locking?'); // Returns: // [ // { skill: { name: 'postgres', description: '...' }, score: 0.82 }, // { skill: { name: 'policy-validation', description: '...' }, score: 0.31 }, // ] // With options const results = await matimo.semanticSearchSkills('rate limiting and retries', { limit: 3, // Max results (default: 5) minScore: 0.3, // Minimum relevance threshold (default: 0.1) }); ``` Embeddings are cached per skill, the first search builds the index, subsequent searches are fast. > **Agent availability:** `semanticSearchSkills` is also exposed as the agent-callable `matimo_search_skills` meta-tool, so LangChain agents and MCP clients (Claude) can run TF-IDF (or custom-embedding) search directly from their tool loop, not just via the SDK. See [Searching and Loading Skills Selectively](#searching-and-loading-skills-selectively). ### Custom Embedding Provider Replace the built-in TF-IDF with OpenAI, Cohere, or any other embedding model: ```typescript import type { EmbeddingProvider } from 'matimo'; class OpenAIEmbeddingProvider implements EmbeddingProvider { async embed(texts: string[]): Promise { const response = await openai.embeddings.create({ model: 'text-embedding-3-small', input: texts, }); return response.data.map(d => d.embedding); } } matimo.setSkillEmbeddingProvider(new OpenAIEmbeddingProvider()); // Now semantic search uses OpenAI embeddings const results = await matimo.semanticSearchSkills('How do I send a Slack message?'); ``` --- ## Agent Skill Lifecycle ### Creating a Skill ```typescript // Agent creates a skill at runtime const result = await matimo.execute('matimo_create_skill', { name: 'data-validation', content: `--- name: data-validation description: Patterns for validating user input and external API responses with Zod. --- # Data Validation Guidelines Always use Zod for runtime validation. Never trust user input without Zod parsing. `, target_dir: './agent-skills', // optional, defaults to ./matimo-tools/skills }); // result.success → true // result.path → './agent-skills/data-validation/SKILL.md' ``` **Requires human approval**, `matimo_create_skill` has `requires_approval: true`. ### Listing Skills ```typescript // Discover available skills (Level 1: name + description only) const result = await matimo.execute('matimo_list_skills', { skills_dir: './agent-skills', // optional, defaults to ./matimo-tools/skills }); // result.skills → [{ name, description, path }, ...] // result.total → number ``` ### Reading a Skill ```typescript // Load full content (Level 2) const result = await matimo.execute('matimo_get_skill', { name: 'data-validation', skills_dir: './agent-skills', }); if (result.success) { // result.content → full SKILL.md text the agent can apply // result.description → frontmatter description } ``` ### Validating a Skill ```typescript // Check spec compliance const result = await matimo.execute('matimo_validate_skill', { name: 'data-validation', skills_dir: './agent-skills', }); if (result.valid) { // Spec-compliant } else { // result.issues → [{ level: 'error' | 'warning', message }] } ``` Validation checks: - `SKILL.md` exists in `{skills_dir}/{name}/` - Frontmatter is parseable YAML - `name` field present and matches directory name - `description` field present - Name follows spec rules (lowercase, hyphens, 1–64 chars, no leading/trailing hyphens) ### Searching and Loading Skills Selectively Three meta-tools wrap the programmatic [`semanticSearchSkills()`](#skill-search-tf-idf), [`getSkillSections()`](#getskillsections-section-inventory), and [`getSkillContent()`](#getskillcontent-selective-loading) SDK APIs as agent-callable tools, so LangChain agents and MCP clients (Claude) can search and selectively load skills from their tool loop, not just from SDK code: ```typescript // Rank skills by meaning instead of exact name/keyword match const search = await matimo.execute('matimo_search_skills', { query: 'rate limiting and retries', limit: 5, // optional, default 10 min_score: 0.1, // optional, default 0.1 }); // search.results → [{ name, description, relevanceScore }, ...] // Inventory a skill's sections and token costs before loading anything const sections = await matimo.execute('matimo_get_skill_sections', { name: 'slack' }); // sections.sections → [{ path, level, tokenEstimate }, ...] // Load only the sections that matter, once you know which ones do const content = await matimo.execute('matimo_get_skill_content', { name: 'slack', sections: ['Messaging'], // optional, omit to return the whole body max_tokens: 500, // optional }); // content.content → targeted section content // content.tokensUsed → approximate token count of what was returned ``` This gives agents a search → inventory → load workflow that costs far less context than `matimo_get_skill` alone: ```typescript matimo_list_skills() // → all skill names + descriptions matimo_get_skill('slack') // → full content matimo_search_skills('rate limiting and retries') // → ranked results with scores matimo_get_skill_sections('slack') // → section inventory with token estimates matimo_get_skill_content('slack', { sections: ['Messaging'] }) // → targeted section content ``` For LangChain agents outside the meta-tool loop, the SDK-level `buildRelevantSkillPrompt()` helper remains the simplest option, it runs TF-IDF search and injects the top-K skill content into your system prompt before the agent loop even starts. See [LangChain Agent with Skills](#langchain-agent-with-skills) for that pattern. --- ## Pluggable Skill Storage: Bring Your Own Backend Matimo OSS doesn't own skill storage, the same way it doesn't ship a database for Tools. A host platform (Matimo Workbench or your own) decides where skills actually live; Matimo OSS only needs two things from you: **a location to read from**, and **a direct way to push content in**. | Your skills live in... | Use | |---|---| | A filesystem, local disk, NFS/EFS/SMB, a synced git checkout, a FUSE-mounted bucket | `skillPaths` (init-time) or `matimo.addSkillPath(path)` (runtime), then `matimo.reloadSkills()` | | Anything else, Postgres, MongoDB, S3's API, an internal service | `matimo.registerSkill(skill)` / `matimo.registerSkills(skills)` | `addSkillPath()` only adds a directory to the list Matimo will scan, call `reloadSkills()` afterward to actually read its `SKILL.md` files. `registerSkill()`/`registerSkills()` take effect immediately with no filesystem round-trip; they accept plain `SkillDefinition` objects (`{ name, description, body, ... }`) fetched however you like. ### Recipe: loading skills from S3 ```typescript import { MatimoInstance } from 'matimo'; import { S3Client, ListObjectsV2Command, GetObjectCommand } from '@aws-sdk/client-s3'; const s3 = new S3Client({}); const matimo = await MatimoInstance.init({ autoDiscover: true }); const { Contents } = await s3.send( new ListObjectsV2Command({ Bucket: 'my-skills', Prefix: 'skills/' }) ); const skills = await Promise.all( (Contents ?? []) .filter((o) => o.Key?.endsWith('SKILL.md')) .map(async (o) => { const obj = await s3.send(new GetObjectCommand({ Bucket: 'my-skills', Key: o.Key! })); const body = await obj.Body!.transformToString(); const name = o.Key!.split('/').at(-2)!; // skills/{name}/SKILL.md const description = /^description:\s*(.+)$/m.exec(body)?.[1] ?? ''; return { name, description, body }; }) ); matimo.registerSkills(skills); ``` ### Recipe: loading skills from Postgres ```typescript import { MatimoInstance } from 'matimo'; import { Pool } from 'pg'; const pool = new Pool(); const matimo = await MatimoInstance.init({ autoDiscover: true }); const { rows } = await pool.query( 'SELECT name, description, body FROM tenant_skills WHERE tenant_id = $1', [tenantId] ); matimo.registerSkills(rows.map((r) => ({ name: r.name, description: r.description, body: r.body }))); ``` Matimo OSS ships no S3 or Postgres client in either recipe, bring whatever library you already use. `registerSkills()` has no opinion on where the rows came from. ### Observing agent-created skills When an agent calls `matimo_create_skill`, Matimo emits a `skill:created` event through the same `onEvent` handler used for tool/policy events, so a host can mirror an agent-created skill into its own storage the moment it happens instead of polling the filesystem: ```typescript const matimo = await MatimoInstance.init({ autoDiscover: true, onEvent: (event) => { if (event.type === 'skill:created') { // event.skillName, event.source ('user' | 'catalog'), event.timestamp // e.g. read the file matimo_create_skill just wrote and push it into your DB } }, }); ``` ### Configuring where agent-created skills get written By default, `matimo_create_skill` writes to `./matimo-tools/skills` unless the caller passes `target_dir` explicitly. Set `defaultSkillWriteDir` once at startup instead of relying on every agent call to pass `target_dir` correctly: ```typescript const matimo = await MatimoInstance.init({ autoDiscover: true, defaultSkillWriteDir: '/var/matimo/tenant-42/skills', }); ``` **Python:** the same API exists under snake_case, `matimo.add_skill_path(path)`, `await matimo.reload_skills()`, `matimo.register_skill(skill)` / `register_skills(skills)`, `Matimo.init(default_skill_write_dir=...)`, and an `on_event` handler that receives a `{"type": "skill:created", "skill_name": ..., "source": ..., "timestamp": ...}` dict. --- ## MCP Server: Skills as Resources When the Matimo MCP server is running, every registered skill is automatically exposed as an **MCP Resource** under the `skills://` URI scheme. MCP clients (Claude Desktop, Cursor, Windsurf, any MCP-compliant client) can read skill content without calling a tool, they use the Resources protocol directly. ### Resource URIs Each skill gets a resource at: ``` skills://{skill-name} ``` | Skill | URI | MIME type | |-------|-----|-----------| | `slack` | `skills://slack` | `text/markdown` | | `github` | `skills://github` | `text/markdown` | | `tool-creation` | `skills://tool-creation` | `text/markdown` | | `code-review` _(agent-created)_ | `skills://code-review` | `text/markdown` | All skills registered in Matimo, built-in SDK skills, provider skills, and agent-created skills, are exposed automatically. No configuration required. ### How Claude accesses skills With the MCP server running (`npx matimo mcp`), Claude has two complementary ways to access skills: **Via Resources (passive read):** Claude can browse and read `skills://` resources without invoking a tool. The content is the full `SKILL.md` text. This is equivalent to Level 2 in the progressive disclosure model, full content on demand. **Via Meta-Tools (agent-driven discovery):** Claude can also use the skill meta-tools to actively discover and interact with skills: - `matimo_list_skills` → Level 1 metadata (names + descriptions) - `matimo_get_skill` → Level 2 full content (same as reading the resource) - `matimo_create_skill` → create a new skill (writes to disk + registers new resource) - `matimo_validate_skill` → check spec compliance The practical difference: reading a `skills://` resource is a direct read by the MCP client; calling `matimo_get_skill` is an agent tool call that goes through Matimo's execution layer. ### Hot-reload When `matimo.reloadTools()` is called (e.g. after `matimo_create_skill` writes a new skill to disk), the MCP server: 1. Removes stale skill resource registrations 2. Re-registers all current skills from `matimo.listSkills()` 3. Calls `sendResourceListChanged()`, MCP clients receive an automatic notification and refresh their resource list Agents and clients never need to reconnect to see newly created skills. ### Starting the MCP server ```bash # Expose all tools + skills over stdio (for Claude Desktop, Cursor, Windsurf) npx matimo mcp # Or HTTP mode (remote / Docker) npx matimo mcp --transport http --port 3000 ``` After connecting, Claude sees: - All tools in the standard MCP `tools/list` - All skills as resources in `resources/list` under `skills://*` See [MCP documentation](https://matimo.dev/docs/mcp-server.md) for the full server setup guide. --- ## LangChain Agent with Skills Skills are pull-based, they're not auto-injected into LLM context. Instead, agents discover and load skills at runtime using meta-tools. Here's a complete LangChain agent that uses skills as context: ```typescript import { MatimoInstance, convertToolsToLangChain } from 'matimo'; import { ChatOpenAI } from '@langchain/openai'; import { HumanMessage, SystemMessage, ToolMessage } from '@langchain/core/messages'; import type { ToolDefinition } from 'matimo'; // Initialize Matimo with skills auto-loaded from providers const matimo = await MatimoInstance.init({ autoDiscover: true }); // Convert tools to LangChain format (includes skill meta-tools) const tools = matimo.listTools(); const langchainTools = await convertToolsToLangChain(tools as ToolDefinition[], matimo); const llm = new ChatOpenAI({ model: 'gpt-4o-mini', temperature: 0 }); const llmWithTools = llm.bindTools(langchainTools); // The agent can now autonomously: // 1. matimo_list_skills(), discover available skills // 2. matimo_get_skill('slack'), load the Slack skill for context // 3. Use skill knowledge to correctly call Slack tools // 4. matimo_create_skill(), create new skills from experience const messages = [ new SystemMessage(`You have access to Matimo skills and tools. Use matimo_list_skills to discover domain knowledge. Use matimo_get_skill to load a skill before working in that domain.`), new HumanMessage('Send a message to #general in Slack saying "Hello from Matimo"'), ]; // Agent loop, LLM discovers skills, loads context, then acts let response = await llmWithTools.invoke(messages); while (response.tool_calls?.length) { messages.push(response); for (const call of response.tool_calls) { const result = await matimo.execute(call.name, call.args); messages.push(new ToolMessage({ tool_call_id: call.id || '', content: JSON.stringify(result), name: call.name, })); } response = await llmWithTools.invoke(messages); } console.log(response.content); ``` **The agent workflow:** 1. LLM calls `matimo_list_skills` → gets names/descriptions of all 10 native provider skills + 6 core skills 2. LLM calls `matimo_get_skill('slack')` → loads full Slack domain knowledge (channels, messages, threads, etc.) 3. LLM now has context to correctly call `slack_send_channel_message` with the right parameters 4. No skill content is wasted on providers the agent doesn't need **Non-MCP usage, programmatic progressive disclosure:** When running LangChain without an MCP server, use the two helper functions to implement the same progressive disclosure pattern in code: ```typescript import { MatimoInstance, getSkillsMetadata, buildRelevantSkillPrompt, } from 'matimo'; import { HumanMessage, SystemMessage } from '@langchain/core/messages'; // Level 1, once at startup (token-safe, names + descriptions only) const meta = getSkillsMetadata(matimo); const metaBlock = meta.map((s) => `- **${s.name}**: ${s.description}`).join('\n'); // Level 2, per request via TF-IDF keyword-relevance search (loads only relevant skills) const userMessage = 'How do I handle rate limits in Slack?'; const skillContext = await buildRelevantSkillPrompt(matimo, userMessage, { topK: 2, // load at most 2 skills minScore: 0.3, // ignore skills below this relevance }); const messages = [ new SystemMessage(`You are a helpful agent.\n\nAvailable skills:\n${metaBlock}`), ...(skillContext ? [new SystemMessage(skillContext)] : []), new HumanMessage(userMessage), ]; ``` `getSkillsMetadata` returns `Array<{ name, description }>`, no file I/O, always cheap. `buildRelevantSkillPrompt` runs TF-IDF cosine similarity ranking and loads full content only for top-K matches above `minScore`. Returns an empty string when no skills are relevant, so it's safe to spread into the messages array unconditionally. **Standalone function vs. instance method, same result, two call styles:** `buildRelevantSkillPrompt(matimo, query, options)` is a free function that takes a `MatimoInstance` as its first argument. If you already hold that instance and would rather call a method on it than import a helper, `matimo.buildSkillPromptContext(query, options)` does exactly the same thing: ```typescript // Standalone function, import buildRelevantSkillPrompt from 'matimo' const skillContext = await buildRelevantSkillPrompt(matimo, userMessage, { topK: 2 }); // Instance method, nothing extra to import const skillContext = await matimo.buildSkillPromptContext(userMessage, { topK: 2 }); ``` Both call `SkillRegistry.semanticSearch()` under the hood and produce byte-identical output for the same inputs, pick whichever reads better at the call site. This is the ergonomic gap Workbench's own `SkillsService` hit (it guessed at `matimo.buildRelevantSkillPrompt(...)` as an instance method before this existed), see [Pluggable Skill Storage](#pluggable-skill-storage-bring-your-own-backend) below for the general "host platform, own storage" story this API is part of. See the [LangChain integration guide](https://matimo.dev/docs/framework-integrations/langchain.md#skills-integration-non-mcp) for the full API reference and examples. --- ## CrewAI Agent with Skills CrewAI has no MCP client, so skills reach a CrewAI agent the same way they reach a non-MCP LangChain agent: `get_skills_metadata()` and `build_relevant_skill_prompt()`, mirrored 1:1 in `matimo.integrations.crewai` (Python only; CrewAI has no TypeScript SDK), inject skill context directly into an `Agent`'s `backstory` or a `Task`'s `description` instead of a system prompt. Here's a complete CrewAI agent that uses skills as context: ```python from crewai import Agent, Crew, Process, Task from matimo import Matimo from matimo.integrations.crewai import ( build_relevant_skill_prompt, convert_tools_to_crewai, ) # Initialize Matimo with skills auto-loaded from providers matimo = await Matimo.init(auto_discover=True) # Level 2: inject the skill(s) relevant to this agent's job into its backstory task_description = "Send a message to #general in Slack saying 'Hello from Matimo'" skill_context = await build_relevant_skill_prompt(matimo, task_description, top_k=2) # Convert tools to CrewAI format (includes skill meta-tools) tools = [t for t in matimo.list_tools() if t.name.startswith("slack") or "skill" in t.name] crewai_tools = convert_tools_to_crewai(tools, matimo) agent = Agent( role="Slack Assistant", goal="Send accurate, well-formed Slack messages.", backstory=f"You are a Slack operations assistant.\n\n{skill_context}", llm="gpt-4o-mini", tools=crewai_tools, ) task = Task( description=task_description, agent=agent, expected_output="Confirmation the message was sent.", ) crew = Crew(agents=[agent], tasks=[task], process=Process.sequential) result = crew.kickoff() ``` **The agent workflow:** 1. `build_relevant_skill_prompt()` runs TF-IDF semantic search over all loaded skills *before* the crew starts, and injects the top-K matches straight into the agent's `backstory`, CrewAI agents don't see a system prompt the way a LangChain ReAct loop does, so injection happens once, up front, rather than per-turn. 2. The agent now has the Slack domain knowledge (channels, messages, threads, etc.) needed to correctly call `slack_send_channel_message`. 3. The agent can still call `matimo_search_skills` / `matimo_get_skill_sections` / `matimo_get_skill_content` mid-task (if bound via `convert_tools_to_crewai()`) to look up *other* skills the backstory injection didn't cover, the two approaches are complementary, not exclusive. `get_skills_metadata` returns `list[{"name": ..., "description": ...}]`, no file I/O, always cheap; use it to list every available skill in a `Task` description without loading full content. `build_relevant_skill_prompt` runs TF-IDF cosine similarity ranking and loads full content only for top-K matches above `min_score`. Returns an empty string when no skills are relevant, so it's safe to concatenate into a backstory unconditionally. See [`python/examples/crewai/skills/skills_crewai.py`](https://github.com/tallclub/matimo/blob/main/python/examples/crewai/skills/skills_crewai.py) for a full runnable example that combines backstory injection with the 3 skill meta-tools bound as CrewAI tools. Like the LangChain section above, `matimo.integrations.crewai.build_relevant_skill_prompt(matimo, task_description, top_k=2)` also has an instance-method equivalent: `await matimo.build_skill_prompt_context(task_description, top_k=2)`. The instance method always calls through `matimo.integrations.langchain`'s implementation (not the CrewAI one), `matimo.integrations.crewai`'s version is a byte-for-byte duplicate kept dependency-free of `langchain-core`, not a re-export, but the two produce identical output for identical inputs, so either call style works from a CrewAI agent. --- ## Storage Paths | Context | Default path | |---------|-------------| | Agent-created skills | `./matimo-tools/skills/{name}/SKILL.md` | | SDK built-in skills | `packages/core/skills/{name}/SKILL.md` | | Custom (via `target_dir`) | `{target_dir}/{name}/SKILL.md` | > Skills created by agents are **permanent**, they are written to `target_dir` on disk, not to a temp location. They survive restarts and can be read back via `matimo_get_skill`. --- ## Name Rules Skill names follow the Agent Skills spec: | Rule | Valid | Invalid | |------|-------|---------| | Lowercase letters and numbers | `api-errors` | `API-Errors` | | Hyphens as separators | `code-review` | `code_review` (underscores) | | 1–64 characters | `my-skill` | (empty or >64 chars) | | No leading/trailing hyphens | `tool-usage` | `-tool-usage` | | No consecutive hyphens | `my-skill` | `my--skill` | --- ## Examples Demo Run the skills demo to see a real LangChain agent create, read, apply, and validate skills. Available in both TypeScript and Python: **TypeScript:** ```bash cd typescript/examples/tools pnpm skills:demo ``` **Python:** ```bash cd python/ make skills-demo # or directly: cd python/examples/native && uv run -w ../.. python skills/skills_demo.py ``` **What it demonstrates:** **Phase 2 / Missions, Agent missions (goal-driven, no tool names given):** 1. Agent creates a `code-review` skill (human approves) 2. Agent lists available skills (Level 1 discovery) 3. Agent reads `code-review` skill and applies its guidelines to review sample code 4. Agent creates a `security-checklist` skill (auto-approved via whitelist) 5. Agent validates both skills against the Agent Skills spec 6. Agent lists, reads, and applies *all* available skills in one pass **Phase 4, Non-MCP progressive disclosure (both SDKs):** - `getSkillsMetadata()` / `get_skills_metadata()`, Level 1: names + descriptions only (no file I/O) - `semanticSearchSkills(query)` / `semantic_search_skills(query)`, raw TF-IDF ranked results with scores per skill - `buildRelevantSkillPrompt(query)` / `build_relevant_skill_prompt(query)`, Level 2: TF-IDF search loads only relevant skill content > **Note:** `semanticSearchSkills` / `semantic_search_skills` is also agent-callable via the `matimo_search_skills` meta-tool (alongside `matimo_get_skill_sections` and `matimo_get_skill_content`), see [Searching and Loading Skills Selectively](#searching-and-loading-skills-selectively). Agents are no longer limited to `matimo_list_skills` + `matimo_get_skill` for discovery. **Docs:** - TypeScript: [`typescript/examples/tools/skills/README.md`](https://github.com/tallclub/matimo/blob/main/typescript/examples/tools/skills/) - Python: [`python/examples/native/skills/skills_demo.py`](https://github.com/tallclub/matimo/blob/main/python/examples/native/skills/skills_demo.py) --- # Tool Specification: YAML Schema Source: https://matimo.dev/docs/tool-development/tool-specification/ Complete guide to writing Matimo tools in YAML. ## Overview Every Matimo tool is defined in a YAML file with a standardized schema. Tools define: - **Metadata**: Name, version, description - **Parameters**: What inputs the tool accepts - **Execution**: How the tool runs (HTTP, command, function) - **Output**: What the tool returns - **Authentication**: How to authenticate (if needed) - **Error Handling**: Retry and recovery logic --- ## Basic Structure ```yaml name: tool-name description: Brief description of what the tool does version: '1.0.0' parameters: # Define input parameters here execution: # Define how to execute the tool output_schema: # Define the output format authentication: # Optional # Define authentication if needed requires_approval: true # Optional, see Governance Fields risk: medium # Optional (required for function tools) error_handling: # Optional # Retry settings (accepted, not yet applied) ``` --- ## Metadata ### name Unique tool identifier. ```yaml name: slack_send_channel_message # newer packages: snake_case name: github-create-issue # older packages: kebab-case name: calculator ``` **Rules:** - Lowercase, no spaces - Globally unique among the tools a `MatimoInstance` loads - New tools use `snake_case`. The GitHub, Gmail, HubSpot, Mailchimp, Twilio and Postgres packages and `slack-send-message` predate that and use kebab-case. Neither style is enforced by the loader, so keep the convention of the package you're adding to ### description What the tool does. One sentence. ```yaml description: Create a new GitHub issue in a repository description: Send a message to a Slack channel description: Perform basic math calculations ``` ### version Semantic versioning: `MAJOR.MINOR.PATCH` ```yaml version: "1.0.0" version: "2.1.3" ``` --- ## Parameters Define what inputs the tool accepts. ### Basic Structure ```yaml parameters: param_name: type: string|number|boolean|object|array description: What this parameter does required: true|false ``` ### Parameter Properties #### type (required) Parameter data type. ```yaml type: string # Text input type: number # Integer or float type: boolean # True/false type: object # JSON object type: array # Array of items ``` #### description (required) What this parameter does. ```yaml description: GitHub repository in owner/repo format description: Number of items to fetch description: Enable verbose logging ``` #### required (optional) Whether the parameter is mandatory. Defaults to optional, so say `required: true` for anything the tool cannot run without. ```yaml required: true # Must be provided required: false # Optional ``` #### default (optional) Default value if not provided. ```yaml default: 10 default: "main" default: false ``` #### enum (optional) List of allowed values. ```yaml enum: - add - subtract - multiply - divide ``` #### examples, items, properties (optional) `examples` lists sample values. `items` describes the element type of an `array` parameter, and `properties` describes the fields of an `object` parameter. Both nest. #### No min, max or pattern keys There is no `validation:` block. A key Matimo doesn't know, such as `minLength`, `min` or `pattern`, is ignored when the YAML loads, so it enforces nothing. State limits in the `description` instead, and enforce them in the API the tool calls. `MatimoInstance.execute()` does not check arguments against `type`, `required` or `enum` either (only a missing `{placeholder}` in an HTTP URL is rejected). Those fields describe the tool to the model: the LangChain adapter turns them into a Zod schema, and the MCP server publishes them as the tool's `inputSchema`. `default` is applied only by the LangChain adapter. ### Examples #### String Parameter ```yaml parameters: message: type: string description: Message to send (1 to 1000 characters) required: true ``` #### Number Parameter with a Default ```yaml parameters: count: type: number description: Number of items to fetch (1 to 100) required: false default: 10 ``` #### Choice Parameter ```yaml parameters: operation: type: string description: Math operation to perform required: true enum: - add - subtract - multiply - divide ``` #### Object Parameter ```yaml parameters: config: type: object description: Configuration object required: true properties: timeout: type: number retries: type: number ``` --- ## Execution Define how the tool runs. ### Type: Command Execute shell commands. ```yaml execution: type: command command: node args: - script.js - '{param1}' - '{param2}' timeout: 5000 ``` **Fields:** - `command` (string, required), The executable. It cannot contain `{placeholders}`; only `args` are templated - `args` (array, optional), Command arguments with parameter substitution - `timeout` (number, optional, default: 30000), Timeout in milliseconds - `cwd`, `shell` (optional), Accepted by the schema. Python applies `cwd`; TypeScript ignores both and runs without a shell The child process gets the parent's environment plus any per-call `credentials`. Python also accepts an `env` map under `execution` and adds it; TypeScript's schema drops `env`, so don't rely on it in a tool meant for both SDKs. A command tool asks a human before every call unless it declares `requires_approval: false`, and agents cannot create one. **Parameter Substitution:** Use `{param_name}` to reference parameters. ```yaml args: - '--operation={operation}' - '{a}' - '{b}' ``` When executed with `{ operation: 'add', a: 5, b: 3 }`, becomes: ``` --operation=add 5 3 ``` ### Type: HTTP Make HTTP requests. ```yaml execution: type: http method: POST url: 'https://api.example.com/endpoint' headers: Content-Type: application/json Authorization: 'Bearer {MATIMO_API_KEY}' body: name: '{name}' timeout: 10000 ``` **Fields:** - `method` (string, required), HTTP method: GET, POST, PUT, DELETE, PATCH - `url` (string, required), API endpoint URL with parameter substitution - `headers` (object, optional), HTTP headers - `body` (any, optional), Request body, with parameter substitution - `query_params` (object, optional), Query string parameters - `parameter_encoding` (array, optional), See [HTTP Parameter Embedding](https://matimo.dev/docs/tool-development/http-parameter-embedding.md) - `timeout` (number, optional), Timeout in milliseconds. Python defaults to 30000; TypeScript sets no timeout unless you give one, so set it explicitly Authentication goes in the top-level `authentication` block (see [Authentication](#authentication)), not under `execution`. **URL Templating:** ```yaml url: 'https://api.github.com/repos/{owner}/{repo}/issues' ``` When executed with `{ owner: 'tallclub', repo: 'matimo' }`: ``` https://api.github.com/repos/tallclub/matimo/issues ``` ### Type: Function Execute a JavaScript/TypeScript module exported as the tool's default function. The `code` field is a path (relative to the tool's YAML file) to the implementation module. ```yaml execution: type: function code: './my_tool.ts' timeout: 30000 ``` The implementation module must export a default async function. Its optional second argument carries the call's per-call credentials and the caller's policy context, both supplied by the host (`execute(..., { credentials, context })`), never by the agent: ```typescript // my_tool.ts import type { FunctionToolContext } from '@matimo/core'; export default async function myTool( params: Record, context?: FunctionToolContext // { credentials?, policyContext?: { agentId, roles, environment } } ): Promise { if (!context?.policyContext?.roles?.includes('admin')) { return { success: false, error: 'admin only' }; } return { result: params.value }; } ``` The Python SDK runs `my_tool.py` next to the YAML (same name, `.py` suffix). Its `run()` may be sync or async, and receives a `FunctionToolContext` when it accepts a second argument: ```python # my_tool.py from matimo import FunctionToolContext def run(params: dict, context: FunctionToolContext | None = None) -> dict: caller = context.policy_context if context else None return {"result": params.get("value"), "agent": caller.agent_id if caller else None} ``` **Fields:** - `code` (string, required), Relative path to the implementation module (`.ts` or `.js`; Python uses the sibling `.py`) - `timeout` (number, optional), Execution timeout in milliseconds Every function tool must declare `risk:`, it is the risk the policy engine uses when the tool runs, since its code can do anything. `pnpm validate-tools` and `make validate-tools` reject a function tool without one. See [Governance Fields](#governance-fields). **Trust model, IMPORTANT:** `execution.type: function` is **blocked for agent-created tools** (`untrusted` source). Agents cannot propose tools with this execution type because it allows arbitrary code execution. Only developer-authored tools in `trustedPaths` (installed `@matimo/*` packages or explicit file paths) may use `type: function`. In TypeScript this is a hard block, the policy engine rejects any `untrusted` tool with `execution.type: function` regardless of policy configuration. The Python SDK rejects it unless the policy sets `allow_function_tools`. If you are building meta-tools (like the built-in `matimo_approve_tool`), use `type: function` freely, they live in trusted paths. --- ## Governance Fields These fields decide how the policy engine treats a tool. All are optional except `risk` on function tools. ```yaml requires_approval: true # ask a human before every call risk: high # low | medium | high | critical status: approved # draft | approved | deprecated deprecated: false deprecation_message: 'Use slack_send_channel_message instead' ``` | Field | Effect | |-------|--------| | `requires_approval` | `true`: every call waits for an approval callback ([Approval System](https://matimo.dev/docs/api-reference/approval-system.md)). Unset: HTTP `DELETE` and `type: command` tools still ask (the 0.2.0 secure default); everything else doesn't. `false`: opts a DELETE or command tool out of that default, the destructive-keyword scan of `sql`/`command` arguments still applies. HTTP DELETE tools in this repo must say `requires_approval: true`; the validator enforces it | | `risk` | Raises the automatically computed risk (GET low; POST/PUT/PATCH medium; DELETE, command, or `requires_approval` high), never lowers it. For function tools it **is** the risk, and is required | | `status` | `draft` tools never run in production and run elsewhere only for an `admin` caller; `deprecated` tools never run. Agent-created tools start as `draft` | | `deprecated` / `deprecation_message` | Same as `status: deprecated`; the message is returned to the caller | Risk feeds HITL quarantine (`enableHITL` + `hitlMinRiskLevel`) and appears on every `tool:executed` event. See [Policy and Lifecycle](https://matimo.dev/docs/api-reference/policy-and-lifecycle.md). --- ## Output Schema Define what the tool returns. ```yaml output_schema: type: object properties: id: type: number name: type: string created_at: type: string required: - id - name ``` ### Supported Types ```yaml type: object # JSON object type: array # Array of items type: string # Text type: number # Integer or float type: boolean # True/false ``` ### Object Schema ```yaml type: object properties: field_name: type: string description: Field description field_count: type: number required: - field_name ``` ### Array Schema ```yaml type: array items: type: object properties: id: type: number name: type: string ``` ### Examples #### Simple Object ```yaml output_schema: type: object properties: result: type: number timestamp: type: string required: - result ``` #### Array of Objects ```yaml output_schema: type: array items: type: object properties: id: type: number title: type: string author: type: string ``` #### Nested Objects ```yaml output_schema: type: object properties: user: type: object properties: id: type: number name: type: string email: type: string status: type: string required: - user - status ``` ### max_response_size Optional. Caps how many bytes of this tool's raw result Matimo will return before truncating it, overriding the instance-wide default. ```yaml output_schema: type: array items: type: object max_response_size: 1048576 # 1 MB, in bytes ``` Every tool call passes through a response-size guardrail in `execute()`, regardless of whether it's invoked directly, via LangChain/CrewAI, or via MCP, so this applies uniformly across every integration. Precedence, highest first: 1. This tool's own `output_schema.max_response_size` 2. The instance-level default passed to `MatimoInstance.init()` (`defaultMaxResponseSize` in TS, `default_max_response_size` in Python) 3. A built-in 256 KB (262,144 byte) default applied to every tool that sets neither of the above When a result exceeds the effective cap, it's truncated rather than rejected: - **Arrays** are sliced to fit the byte budget, with a sentinel element appended: `"...truncated, N of M items shown"`. - **Long strings** are sliced with an inline `"...truncated, N of M characters shown"` marker. - **Large objects** are truncated per-field (smaller fields like `statusCode`/`headers` are kept intact; the largest field absorbs the cut), and the outermost object gets an additive `_truncated: true` field, existing keys are never renamed or removed. See [Tool Execution Flow](https://matimo.dev/docs/architecture/overview.md#tool-execution-flow) for where this fits in the request lifecycle, and [Error Codes Reference](https://matimo.dev/docs/api-reference/errors.md) for how a truncated result differs from an error response (it's never an error, the result is still returned, just capped). --- ## Authentication Describe how the tool authenticates (optional). ```yaml authentication: type: none|api_key|basic|bearer|oauth2|custom location: header|query|body name: header_name provider: provider_name # oauth2 only required: true username_env: ENV_NAME # basic only password_env: ENV_NAME # basic only ``` There is no `secret_env_var` field. Apart from `basic`, the `authentication` block describes the scheme and is not what injects the secret. The secret reaches the request through a `{PLACEHOLDER}` in `headers`, `query_params`, `url` or `body`: ```yaml execution: type: http method: GET url: 'https://api.example.com/items' headers: Authorization: 'Bearer {EXAMPLE_API_KEY}' authentication: type: bearer location: header name: Authorization ``` Only a placeholder whose name contains `token`, `key`, `secret`, `password`, `credential`, `auth` or `bearer` is treated as a credential, so name yours that way. At call time Matimo fills `{EXAMPLE_API_KEY}` from the first of these that is set (see [Authentication](https://matimo.dev/docs/user-guide/authentication.md)): 1. The call's `credentials`: `EXAMPLE_API_KEY`, then `MATIMO_EXAMPLE_API_KEY` 2. The environment variable `MATIMO_EXAMPLE_API_KEY` 3. The environment variable `EXAMPLE_API_KEY` ### Type: Basic Auth `basic` is the one scheme the HTTP executor handles itself. Name the two variables that hold the username and password, and it builds the `Authorization: Basic ...` header: ```yaml authentication: type: basic username_env: TWILIO_ACCOUNT_SID password_env: TWILIO_AUTH_TOKEN ``` If either variable is unset (and not passed in `credentials`) the call fails with an error naming both. ### Type: OAuth2 ```yaml authentication: type: oauth2 provider: github ``` `provider` names a provider definition that holds the authorization and token URLs. Matimo does not run the browser flow or refresh tokens for you: your application obtains the token (see [OAuth2 and credentials](https://matimo.dev/docs/architecture/oauth.md)) and supplies it as the placeholder value. --- ## Error Handling > **Not applied yet.** `error_handling` is validated and kept on the tool definition, but neither SDK's executors read it today: a failed call is not retried. Retries with these settings are planned. Errors carry `details.retryable` (set for timeouts, 429 and 5xx responses) so a caller can retry itself. Retry settings (optional). ```yaml error_handling: retry: 3 backoff_type: exponential|linear|fixed initial_delay_ms: 1000 max_delay_ms: 30000 ``` **Fields:** - `retry` (number), Number of retry attempts - `backoff_type` (string), `exponential`, `linear` or `fixed` - `initial_delay_ms` (number), Initial delay between retries - `max_delay_ms` (number), Maximum delay between retries ### Backoff Strategies #### exponential Wait time = initial_delay × (2 ^ attempt_number) ```yaml error_handling: retry: 3 backoff_type: exponential initial_delay_ms: 1000 max_delay_ms: 30000 ``` Delays: 1s, 2s, 4s, 8s (capped at 30s) #### linear Wait time = initial_delay × (attempt_number + 1) ```yaml backoff_type: linear initial_delay_ms: 1000 ``` Delays: 1s, 2s, 3s, 4s #### fixed Wait time = initial_delay ```yaml backoff_type: fixed initial_delay_ms: 2000 ``` Delays: 2s, 2s, 2s, 2s --- ## Complete Examples ### Add Two Numbers (command tool) A minimal `type: command` tool. Arguments are passed to the process as separate `args` entries, never through a shell, so a parameter value cannot inject extra commands. `{placeholder}` only substitutes values; it does not evaluate expressions. ```yaml name: add_numbers description: Add two numbers version: '1.0.0' risk: low requires_approval: false # command tools ask a human before every call unless they opt out parameters: a: type: number description: First number required: true b: type: number description: Second number required: true execution: type: command command: node args: - -e - 'console.log(JSON.stringify({ result: Number(process.argv[1]) + Number(process.argv[2]) }))' - '{a}' - '{b}' output_schema: type: object properties: result: type: number ``` The built-in `calculator` is a `type: function` tool with a sandboxed expression parser (`typescript/packages/core/tools/calculator/definition.yaml`). It is a better model for anything beyond arithmetic. ### GitHub Create Issue From `typescript/packages/github/tools/create-issue/definition.yaml` (abridged): ```yaml name: github-create-issue description: Create a new issue in a repository version: '1.0.0' requires_approval: true parameters: owner: type: string required: true description: Repository owner username or organization repo: type: string required: true description: Repository name title: type: string required: true description: Issue title body: type: string required: false description: Issue description (markdown supported) labels: type: array required: false description: Array of label names execution: type: http method: POST url: 'https://api.github.com/repos/{owner}/{repo}/issues' headers: Accept: application/vnd.github+json Authorization: 'Bearer {GITHUB_TOKEN}' X-GitHub-Api-Version: '2022-11-28' Content-Type: application/json body: title: '{title}' body: '{body}' labels: '{labels}' # an array parameter is sent as a JSON array timeout: 15000 authentication: type: bearer location: header output_schema: type: object properties: number: type: number description: Issue number in repository title: type: string html_url: type: string ``` ### Slack Send Message From `typescript/packages/slack/tools/slack_send_channel_message/definition.yaml`: ```yaml name: slack_send_channel_message description: Post a message (text, markdown, blocks) to a public/private Slack channel. version: '1.0.0' parameters: channel: type: string required: true description: Channel ID or name to post the message to text: type: string required: false description: Plain-text message (optional if blocks provided, recommended as fallback for accessibility) execution: type: http method: POST url: 'https://slack.com/api/chat.postMessage' headers: Authorization: 'Bearer {SLACK_BOT_TOKEN}' Content-Type: application/json body: channel: '{channel}' text: '{text}' timeout: 15000 authentication: type: api_key location: header name: Authorization notes: env: SLACK_BOT_TOKEN ``` --- ## Best Practices 1. **Naming**: Lowercase and globally unique; new tools use `snake_case` (`slack_send_channel_message`), though older packages use kebab-case 2. **Description**: Clear, one sentence explaining purpose 3. **Parameters**: A description on every parameter; use `enum` for fixed values and state limits in the description (there are no min/max/pattern keys) 4. **Output Schema**: Document the response fields the caller needs 5. **Authentication**: `{UPPER_CASE}` placeholders filled from the environment; never hardcode a secret 6. **Governance**: `requires_approval: true` on anything destructive, and `risk:` on function tools 7. **Timeout**: Set appropriate timeouts (avoid infinite hangs) 8. **Testing**: Include examples of real-world usage --- ## File Organization Store tools in provider directories. Each tool can be a single file or a subdirectory with its own `definition.yaml`: ``` tools/ ├── provider-name/ │ ├── definition.yaml # Single tool per provider │ └── ... ├── multi-tool-provider/ │ ├── tool-1/ │ │ └── definition.yaml # Multi-tool provider │ └── tool-2/ │ └── definition.yaml └── examples/ └── example-tool.yaml ``` Example: ``` typescript/packages/gmail/ ├── definition.yaml # Provider manifest (OAuth2 endpoints), not a tool └── tools/ ├── send-email/ │ └── definition.yaml ├── create-draft/ │ └── definition.yaml └── list-messages/ └── definition.yaml typescript/packages/core/tools/ └── calculator/ ├── definition.yaml └── calculator.ts # Implementation for a `type: function` tool ``` **Naming conventions:** - Tool subdirectories hold one tool each and are named after it (`send-email`, `slack_send_channel_message`) - Files: Always `definition.yaml` for tool definitions - A `definition.yaml` at a provider's root is the provider manifest, not a tool, and is not counted among its tools --- ## See Also - [Quick Start](https://matimo.dev/docs/getting-started/quick-start.md): Get started in 5 minutes - [API Reference](https://matimo.dev/docs/api-reference/sdk.md): Complete SDK documentation - [Decorator Guide](https://matimo.dev/docs/tool-development/decorator-guide.md): Use decorators - [CONTRIBUTING.md](https://github.com/tallclub/matimo/blob/main/CONTRIBUTING.md): Development guide --- # YAML Tool Specification Source: https://matimo.dev/docs/tool-development/yaml-tools/ Complete guide to writing Matimo tools in YAML format. ## Quick Reference See the [detailed specification](https://matimo.dev/docs/tool-development/tool-specification.md) for complete documentation. **Quick tool template:** ```yaml name: my_tool description: Brief description version: '1.0.0' parameters: query: type: string description: What this parameter does required: true execution: type: http method: GET url: 'https://api.example.com/search' query_params: q: '{query}' output_schema: type: object properties: result: type: string ``` ## Key Sections | Section | Purpose | | ---------------- | ----------------------------------- | | `name` | Unique tool identifier (snake_case, matching its directory) | | `description` | One-line description | | `version` | Semantic version (e.g., 1.0.0) | | `parameters` | Tool input parameters | | `execution` | How the tool runs (HTTP, command or function) | | `output_schema` | What the tool returns; `max_response_size` caps it | | `authentication` | OAuth2/API key config (optional) | | `requires_approval` | Ask a human before every call (optional; DELETE and command tools ask by default) | | `risk` | low / medium / high / critical (optional; required for function tools) | | `status` | draft / approved / deprecated (optional) | | `error_handling` | Retry settings (optional; accepted but not yet applied) | ## Full Specification The complete YAML schema documentation is in [TOOL_SPECIFICATION.md](https://matimo.dev/docs/tool-development/tool-specification.md). Topics covered: - Metadata (name, description, version) - Parameter types and constraints - Execution modes (HTTP, command, function) - Governance fields (approval, risk, status) - Authentication configuration - Output schema validation and response-size caps - Error handling settings - Examples for each type ## Next Steps - **Full Specification**: [TOOL_SPECIFICATION.md](https://matimo.dev/docs/tool-development/tool-specification.md) - **Test Your Tool**: [Testing Guide](https://matimo.dev/docs/tool-development/testing.md) - **Decorator Pattern**: [Decorator Guide](https://matimo.dev/docs/tool-development/decorator-guide.md) --- # Authentication Source: https://matimo.dev/docs/user-guide/authentication/ Tools that call an authenticated API name their credential in the YAML (for example `Authorization: 'Bearer {GITHUB_TOKEN}'`). Matimo fills it in at call time; you supply the value through the environment or per call. ## Quick Start ```bash export GITHUB_TOKEN="ghp_..." # or MATIMO_GITHUB_TOKEN ``` ```typescript import { MatimoInstance } from '@matimo/core'; const matimo = await MatimoInstance.init({ autoDiscover: true }); const repo = await matimo.execute('github-get-repository', { owner: 'tallclub', repo: 'matimo' }); ``` For a token per user (multi-tenant), pass it with the call instead of the environment: ```typescript await matimo.execute( 'github-get-repository', { owner: 'tallclub', repo: 'matimo' }, { credentials: { GITHUB_TOKEN: tokenForThisUser } } ); ``` ```python await matimo.execute( "github-get-repository", {"owner": "tallclub", "repo": "matimo"}, credentials={"GITHUB_TOKEN": token_for_this_user}, ) ``` ## Lookup Order For each credential placeholder, Matimo uses the first value it finds: 1. The call's `credentials`, `NAME`, then `MATIMO_NAME` 2. `MATIMO_NAME` in the environment 3. `NAME` in the environment A placeholder counts as a credential when its name contains `TOKEN`, `KEY`, `SECRET`, `PASSWORD`, `CREDENTIAL`, `AUTH`, `BEARER` or `API_KEY`. Details: [OAuth2 and Credentials](https://matimo.dev/docs/architecture/oauth.md). ## Variables by Provider | Package | Variable(s) | How to get it | |---------|-------------|---------------| | `@matimo/github` | `GITHUB_TOKEN` | [Personal access token](https://github.com/settings/tokens) with the scopes the tools need (e.g. `repo`) | | `@matimo/slack` | `SLACK_BOT_TOKEN` | Slack app → OAuth & Permissions → Bot User OAuth Token (`xoxb-…`), with scopes such as `chat:write`, `channels:read` | | `@matimo/gmail` | `GMAIL_ACCESS_TOKEN` | A Google OAuth2 access token with Gmail scopes (e.g. from the [OAuth Playground](https://developers.google.com/oauthplayground) or `OAuth2Handler`) | | `@matimo/notion` | `NOTION_API_KEY` | Notion integration secret; share the pages with the integration | | `@matimo/hubspot` | `MATIMO_HUBSPOT_API_KEY` | HubSpot private app access token | | `@matimo/mailchimp` | `MAILCHIMP_API_KEY` | Mailchimp API key (the tools also take `server_prefix`, e.g. `us6`) | | `@matimo/twilio` | `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN` | Twilio console; used for basic auth | | `@matimo/microsoft` | `MICROSOFT_GRAPH_ACCESS_TOKEN` | A delegated Microsoft Graph token | | `@matimo/postgres` | `MATIMO_POSTGRES_URL`, or `MATIMO_POSTGRES_HOST` / `_PORT` / `_USER` / `_PASSWORD` / `_DB` | Your database | | `@matimo/composio` | `COMPOSIO_API_KEY` | Composio dashboard; each call also takes the user's connected account ID | The exact name is in each tool's YAML (`headers`, `query_params`, or `authentication.username_env` / `password_env`) and in its `notes.env`: ```typescript const tool = matimo.getTool('slack_send_channel_message'); console.log(tool?.execution); // headers: { Authorization: 'Bearer {SLACK_BOT_TOKEN}', … } ``` ## Verify ```bash matimo doctor # checks installed packages for missing credential variables ``` ```typescript matimo.getRequiredCredentials('github-get-repository'); // ['GITHUB_TOKEN'] (TypeScript only) ``` In TypeScript, a call whose auth header can't be filled fails before anything is sent: ``` AUTH_FAILED: Authentication credentials are missing for tool "github-get-repository". • GITHUB_TOKEN → MATIMO_GITHUB_TOKEN (or pass via credentials option) ``` In Python the request is sent and the API's 401 becomes `AUTH_FAILED`. ## Troubleshooting | Symptom | Cause | Fix | |---------|-------|-----| | `Authentication credentials are missing` | No value for the placeholder | Set the variable from the table, or pass `credentials` | | `AUTH_FAILED` with `statusCode: 401` | Token wrong, expired or revoked | Generate or refresh the token | | `AUTH_FAILED` with `statusCode: 403` | Token lacks a scope or permission | Add the scope (and, for Slack, invite the bot to the channel) | | Works locally, fails in another process | The variable isn't set there | Set it in that environment, or pass `credentials` | ## Security - Never commit tokens. Use a `.env` file listed in `.gitignore`, or a secret manager. - Use the narrowest scopes the tools need. - In servers that act for several users, pass tokens per call with `credentials`; don't put one user's token in a shared process's environment. - Never pass a token as a tool parameter: parameters are visible to the model and to approval prompts. ## Next Steps - [OAuth2 and Credentials](https://matimo.dev/docs/architecture/oauth.md): the full lookup rules and the `OAuth2Handler` flow - [Tool Discovery](https://matimo.dev/docs/user-guide/tool-discovery.md) - [Error Codes](https://matimo.dev/docs/api-reference/errors.md) --- # SDK Usage Patterns Source: https://matimo.dev/docs/user-guide/sdk-patterns/ Learn the three main ways to use Matimo SDK, from simplest to most powerful. Both **TypeScript** and **Python** SDKs support all three patterns. - [Python SDK Patterns](#python-sdk) - [TypeScript SDK Patterns](#typescript-sdk) --- ## Python SDK ### Pattern 1: Factory Pattern The simplest way, initialise once, call `execute()` anywhere. #### Basic Setup ```python import asyncio from matimo import Matimo async def main(): # Built-in tools plus every installed matimo-* provider package matimo = await Matimo.init(auto_discover=True) # Your own tools as well: # matimo = await Matimo.init('./tools', auto_discover=True) ``` #### Execute a Tool ```python result = await matimo.execute('calculator', { 'operation': 'add', 'a': 5, 'b': 3, }) print(result) # {'result': 8} ``` #### Discover Tools ```python # List all tools all_tools = matimo.list_tools() print(f"Loaded {len(all_tools)} tools") # Get a specific tool tool = matimo.get_tool('calculator') print(f"Tool: {tool.name} - {tool.description}") # Search tools results = matimo.search_tools('slack') for t in results: print(f"Found: {t.name}") ``` #### Handle Errors ```python from matimo.errors import MatimoError, ErrorCode try: result = await matimo.execute('slack_send_channel_message', { 'channel': '#general', 'text': 'Hello', }) except MatimoError as e: if e.code == ErrorCode.TOOL_NOT_FOUND: print(f"Tool not available: {e}") elif e.code == ErrorCode.POLICY_DENIED: print(f"Blocked by policy: {e.details.get('reason')}") elif e.code == ErrorCode.AUTH_FAILED: print("Check SLACK_BOT_TOKEN") else: print(f"{e.code}: {e} {e.details}") ``` #### Multi-Tenant: Per-Call Credentials ```python # Pass credentials per call instead of environment variables result = await matimo.execute( 'slack_send_channel_message', {'channel': '#general', 'text': 'Hello'}, credentials={'SLACK_BOT_TOKEN': user_token}, ) ``` #### Complete Example ```python import asyncio from matimo import Matimo async def main(): # 1. Initialise matimo = await Matimo.init(auto_discover=True) # 2. List available tools tools = matimo.list_tools() print(f"Loaded {len(tools)} tools") # 3. Execute tool result = await matimo.execute('slack_send_channel_message', { 'channel': '#general', 'text': 'Hello from Matimo!', }) print('Message sent:', result) asyncio.run(main()) ``` **Best for:** Scripts, CLI tools, async backends, cron jobs, webhooks --- ### Pattern 2: Decorator Pattern Ideal for class-based agents, bind tools directly to method signatures. #### Basic Setup ```python from matimo import Matimo from matimo.decorators import tool, set_global_matimo_instance async def setup(): matimo = await Matimo.init(auto_discover=True) # Make instance available to all @tool decorators set_global_matimo_instance(matimo) return matimo ``` #### Define Methods with @tool ```python class EmailBot: @tool('slack_send_channel_message') async def send_message(self, channel: str, text: str): ... # Body is ignored, decorator executes the tool @tool('slack-list-channels') async def list_channels(self): ... ``` #### How the Decorator Works ``` 1. Call: bot.send_message('#general', 'Hello!') 2. Decorator maps positional/keyword args to tool params: → {'channel': '#general', 'text': 'Hello!'} 3. Calls: await matimo.execute('slack_send_channel_message', {...}) 4. Returns result to caller ``` #### Advanced: Multi-Tool Orchestration ```python from matimo import Matimo from matimo.decorators import tool, set_global_matimo_instance class SmartAssistant: @tool('calculator') async def calculate(self, operation: str, a: float, b: float): ... @tool('slack_send_channel_message') async def notify_slack(self, channel: str, text: str): ... # Orchestration method: not decorated async def run_daily_report(self, channel: str): result = await self.calculate('add', 100, 50) await self.notify_slack(channel, f"Daily total: {result['result']}") # Usage async def main(): matimo = await Matimo.init(auto_discover=True) set_global_matimo_instance(matimo) assistant = SmartAssistant() await assistant.run_daily_report('#reports') ``` **Best for:** Class-based agents, frameworks using method dispatch --- ### Pattern 3: LangChain / Framework Integration Convert Matimo tools to LangChain `StructuredTool` objects in one call. ```python import asyncio import os from langchain.agents import create_agent # LangChain 1.x from langchain_openai import ChatOpenAI from matimo import ApprovalRequest, Matimo from matimo.integrations.langchain import convert_tools_to_langchain async def confirm(request: ApprovalRequest) -> bool: return input(f"Allow {request.tool_name} {request.params}? [y/N] ").strip().lower() == "y" async def main() -> None: # 1. Load Matimo tools; writes that need approval ask confirm() matimo = await Matimo.init(auto_discover=True, on_approval=confirm) # 2. Convert only the tools this agent needs (OpenAI accepts at most 128) slack_tools = [t for t in matimo.list_tools() if t.name.startswith("slack")] lc_tools = convert_tools_to_langchain( slack_tools, matimo, credentials={"SLACK_BOT_TOKEN": os.environ["SLACK_BOT_TOKEN"]}, ) # 3. Build and run the agent agent = create_agent(ChatOpenAI(model="gpt-4o-mini"), tools=lc_tools) result = await agent.ainvoke({"messages": [("user", "List all Slack channels")]}) print(result["messages"][-1].content) asyncio.run(main()) ``` **Install:** ```bash pip install "matimo[langchain]" langchain-openai ``` **Best for:** AI agents with automatic tool selection (LangChain, CrewAI) See [LangChain Integration Guide](https://matimo.dev/docs/framework-integrations/langchain.md) for the full reference. --- ## TypeScript SDK ## Pattern 1: Factory Pattern (Recommended for Simple Use Cases) The factory pattern is the simplest and most ergonomic way to use Matimo. ### Basic Setup ```typescript import { MatimoInstance } from 'matimo'; // Built-in tools plus every installed @matimo/* provider package const matimo = await MatimoInstance.init({ autoDiscover: true }); // Your own tools as well: MatimoInstance.init({ autoDiscover: true, toolPaths: ['./tools'] }) // A path alone (init('./tools')) loads only that directory, without the built-in tools. ``` ### Execute a Tool ```typescript const result = await matimo.execute('calculator', { operation: 'add', a: 5, b: 3, }); console.log(result); // { result: 8 } ``` ### Discover Tools ```typescript // List all tools const allTools = matimo.listTools(); console.log(`Loaded ${allTools.length} tools`); // Get specific tool const tool = matimo.getTool('calculator'); console.log(`Tool: ${tool.name} - ${tool.description}`); // Search tools const results = matimo.searchTools('email'); results.forEach((t) => console.log(`Found: ${t.name}`)); ``` ### Handle Errors A call can fail in two ways: Matimo **throws** a `MatimoError` (unknown tool, policy denial, refused approval, HTTP error), or the tool **returns** `{ success: false, error, code }` (TypeScript's built-in tools report bad input this way; Python's raise `EXECUTION_FAILED` instead). ```typescript import { MatimoError } from '@matimo/core'; try { const result = (await matimo.execute('calculator', { operation: 'divide', a: 10, b: 0, })) as { success?: boolean; error?: string; code?: string }; if (result.success === false) { // → { success: false, error: 'Division by zero', code: 'EXECUTION_FAILED', ... } console.error(`Tool reported ${result.code}: ${result.error}`); } } catch (error) { if (error instanceof MatimoError) { if (error.code === 'TOOL_NOT_FOUND') { console.error('Tool not available:', error.message); } else if (error.code === 'POLICY_DENIED') { console.error('Blocked by policy:', error.details?.reason); } else { console.error(`${error.code}:`, error.message); } } } ``` ### Complete Example ```typescript import { MatimoInstance } from 'matimo'; async function main() { try { // 1. Initialize const matimo = await MatimoInstance.init({ autoDiscover: true }); // 2. List available tools const tools = matimo.listTools(); console.log(`Loaded ${tools.length} tools`); // 3. Execute tool const result = await matimo.execute('slack-send-message', { channel: '#general', text: 'Hello from Matimo!', }); console.log('Message sent:', result); } catch (error) { console.error('Error:', error.message); } } main(); ``` **Best for:** Scripts, CLI tools, backend services, cron jobs, webhooks --- ## Pattern 2: Decorator Pattern (Recommended for Class-Based Code) The decorator pattern is ideal for class-based agents and applications with automatic method-to-tool binding. ### Basic Setup ```typescript import { tool, MatimoInstance, setGlobalMatimoInstance } from 'matimo'; // Initialize Matimo const matimo = await MatimoInstance.init({ autoDiscover: true }); // Set global instance for decorators to use setGlobalMatimoInstance(matimo); // Create agent class class EmailBot { @tool('slack-send-message') async sendMessage(channel: string, text: string) { // Decorator intercepts this call // Arguments map to tool parameters: channel, text // Method body is optional - decorator does the execution } @tool('slack-list-channels') async listChannels() { // Decorator handles execution // Returns the tool result directly } } // Use it const bot = new EmailBot(); // When you call bot.sendMessage(), the decorator: // 1. Maps arguments to parameters: { channel: '#general', text: '...' } // 2. Calls matimo.execute('slack_send_channel_message', {...}) // 3. Returns the result await bot.sendMessage('#general', 'Hello from decorator!'); const channels = await bot.listChannels(); ``` ### How @tool Decorator Works **Step 1:** Define method with @tool decorator ```typescript @tool('slack-send-message') async sendMessage(channel: string, text: string) { // Method body is optional (decorator replaces execution) } ``` **Step 2:** Call the method naturally ```typescript await bot.sendMessage('#general', 'Hello!'); ``` **Step 3:** Decorator intercepts and executes ``` 1. Intercept call: sendMessage('#general', 'Hello!') 2. Map args to params: { channel: '#general', text: 'Hello!' } 3. Call matimo.execute('slack_send_channel_message', {...}) 4. Return result to caller ``` ### Advanced: Multi-Tool Orchestration ```typescript import { tool, MatimoInstance, setGlobalMatimoInstance } from 'matimo'; class SmartAssistant { constructor(private matimo: MatimoInstance) {} @tool('calculator') async calculate(operation: string, a: number, b: number) { // Decorated method for math } @tool('slack-send-message') async notifySlack(channel: string, text: string) { // Decorated method for Slack } // Custom orchestration method (not decorated) async runDailyReport(channel: string) { // Custom logic using decorated tools const result = await this.calculate('add', 100, 50); await this.notifySlack(channel, `Daily calculation complete: ${result}`); } } // Usage const matimo = await MatimoInstance.init({ autoDiscover: true }); setGlobalMatimoInstance(matimo); const assistant = new SmartAssistant(matimo); await assistant.runDailyReport('#reports'); ``` **Best for:** Class-based agents, object-oriented design, multi-tool orchestration --- ## Pattern 3: LangChain Integration (Recommended for AI Agents) For intelligent tool orchestration where the LLM automatically decides which tool to use. ### Basic Setup with convertToolsToLangChain ```typescript import { MatimoInstance, convertToolsToLangChain } from '@matimo/core'; import { ChatOpenAI } from '@langchain/openai'; import { createAgent } from 'langchain'; // 1. Load Matimo tools; writes that need approval ask the callback const matimo = await MatimoInstance.init({ autoDiscover: true, onApproval: async (request) => confirmWithUser(request), }); // 2. Convert only the tools this agent needs (OpenAI accepts at most 128) const slackTools = matimo.listTools().filter((t) => t.name.startsWith('slack')); const langchainTools = await convertToolsToLangChain(slackTools, matimo, { SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN!, }); // 3. Create and run the agent const agent = createAgent({ model: new ChatOpenAI({ model: 'gpt-4o-mini' }), tools: langchainTools, }); const result = await agent.invoke({ messages: [{ role: 'user', content: 'Send a Slack message to #general saying "Hello!"' }], }); console.log(result.messages[result.messages.length - 1].content); ``` ### Complete LangChain Example ```typescript import { MatimoInstance, convertToolsToLangChain } from '@matimo/core'; import { ChatOpenAI } from '@langchain/openai'; import { createAgent } from 'langchain'; async function runSlackAgent() { // Initialize; sending a message asks confirmWithUser() only if the tool requires approval const matimo = await MatimoInstance.init({ autoDiscover: true, onApproval: async (request) => confirmWithUser(request), }); // Get Slack tools only const slackTools = matimo.listTools().filter((t) => t.name.startsWith('slack-') || t.name.startsWith('slack_')); console.log(`Loaded ${slackTools.length} Slack tools`); // Convert to LangChain 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, }); // Example queries const queries = [ 'List all Slack channels', 'Send a message to #general saying hello', 'Get user info for john', ]; 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); ``` **Best for:** AI agents with automatic tool selection, natural language interfaces, intelligent workflows --- ## Comparison: All Three Patterns | Feature | Factory | Decorator | LangChain | | -------------------- | ----------------- | ------------------- | --------------- | | **Simplicity** | Simple | Medium | Medium | | **Best For** | Scripts, backends | Classes, multi-tool | AI agents | | **Tool Selection** | Manual (code) | Manual (code) | Automatic (LLM) | | **Type Safety** | Good | Excellent | Good | | **Framework** | Any | Class-based | LangChain+ | | **Learning Curve** | Low | Medium | Medium | | **Production Ready** | Yes | Yes | Yes | --- ## Real-World Examples ### Example 1: Backend API (Factory) ```typescript // Express.js route handler import express from 'express'; import { MatimoInstance } from 'matimo'; const app = express(); const matimo = await MatimoInstance.init({ autoDiscover: true }); app.post('/api/send-slack', async (req, res) => { try { const result = await matimo.execute('slack-send-message', { channel: req.body.channel, text: req.body.text, }); res.json({ success: true, result }); } catch (error) { res.status(400).json({ error: error.message }); } }); app.listen(3000); ``` ### Example 2: Class-Based Service (Decorator) ```typescript // NestJS-style service import { tool, MatimoInstance, setGlobalMatimoInstance } from 'matimo'; export class NotificationService { @tool('slack-send-message') async sendSlack(channel: string, message: string) {} @tool('gmail-send-email') async sendEmail(to: string, subject: string, body: string) {} async notifyUser(userId: string, message: string) { // Use decorated methods naturally await this.sendSlack('#notifications', `User ${userId}: ${message}`); await this.sendEmail('admin@example.com', 'Notification', message); } } // Usage const matimo = await MatimoInstance.init({ autoDiscover: true }); setGlobalMatimoInstance(matimo); const service = new NotificationService(); await service.notifyUser('user123', 'Important update'); ``` ### Example 3: AI Agent (LangChain) ```typescript // ChatGPT-powered agent import { MatimoInstance, convertToolsToLangChain } from '@matimo/core'; import { ChatOpenAI } from '@langchain/openai'; import { createAgent } from 'langchain'; const matimo = await MatimoInstance.init({ autoDiscover: true, onApproval: async (request) => confirmWithUser(request), }); // Give the agent the tools for this job, not all ~150 const picked = matimo .listTools() .filter((t) => t.name.startsWith('gmail') || t.name.startsWith('slack')); const tools = await convertToolsToLangChain(picked, matimo); const agent = createAgent({ model: new ChatOpenAI({ model: 'gpt-4o-mini' }), tools }); // Agent picks tools based on user intent const result = await agent.invoke({ messages: [{ role: 'user', content: 'Send an email and post to Slack about the Q4 report' }], }); ``` --- ## Error Handling Best Practices ```typescript import { MatimoError, ErrorCode } from '@matimo/core'; try { const result = await matimo.execute('calculator', { operation: 'add', a: 5, b: 3, }); } catch (error) { if (error instanceof MatimoError) { // Handle Matimo-specific errors switch (error.code) { case ErrorCode.TOOL_NOT_FOUND: console.error(`Tool "${error.details?.toolName}" not found`); break; case ErrorCode.POLICY_DENIED: console.error(`Blocked by policy:`, error.details?.reason); break; case ErrorCode.EXECUTION_FAILED: console.error(`Execution failed:`, error.details); break; case ErrorCode.AUTH_FAILED: console.error(`Authentication failed:`, error.message); break; default: console.error(`Unknown error:`, error.message); } } else { // Handle unexpected errors console.error('Unexpected error:', error); } } ``` --- ## Debugging Tips ### Log Available Tools ```typescript const matimo = await MatimoInstance.init({ autoDiscover: true }); const tools = matimo.listTools(); console.log('Available tools:'); tools.forEach((t) => { console.log(` - ${t.name}: ${t.description}`); console.log(` Parameters:`, Object.keys(t.parameters || {})); }); ``` ### Validate Tool Before Executing ```typescript const toolName = 'calculator'; const tool = matimo.getTool(toolName); if (!tool) { console.error(`Tool "${toolName}" not found`); return; } console.log('Tool definition:', tool); ``` ### Enable Detailed Error Messages ```typescript try { await matimo.execute('tool-name', params); } catch (error) { console.error('Full error:', JSON.stringify(error, null, 2)); } ``` --- ## Next Steps - **[Framework Integrations](https://matimo.dev/docs/framework-integrations/langchain.md)**: LangChain details & examples - **[Tool Discovery Guide](https://matimo.dev/docs/user-guide/tool-discovery.md)**: Finding and managing tools - **[API Reference](https://matimo.dev/docs/api-reference/sdk.md)**: Complete SDK documentation - **[Examples](https://github.com/tallclub/matimo/blob/main/typescript/examples/)**: Working code for all patterns See [examples/tools/](https://github.com/tallclub/matimo/blob/main/typescript/examples/tools/) for complete, runnable examples. --- # Tool Discovery & Filtering Source: https://matimo.dev/docs/user-guide/tool-discovery/ Load tools, then find the ones you need by name, tag, text or definition. ## Loading Tools ```typescript import { MatimoInstance } from '@matimo/core'; const matimo = await MatimoInstance.init({ autoDiscover: true, // built-in tools + every installed @matimo/* package toolPaths: ['./tools'], // optional: your own tools }); console.log(matimo.listTools().length); // e.g. 153 with all providers installed ``` ```python from matimo import Matimo matimo = await Matimo.init("./tools", auto_discover=True) print(len(matimo.list_tools())) ``` - **`autoDiscover` / `auto_discover`** loads the core tools (`calculator`, `web`, `read`, `edit`, `execute`, the `matimo_*` meta-tools, …) and the tools of every installed provider package: `@matimo/*` in `node_modules`, or `matimo-*` packages in the Python environment. - **`toolPaths`** adds directories of your own. They are scanned recursively for `definition.yaml` files, so `tools/my-provider/my_tool/definition.yaml` works. - **A path alone** (`init('./tools')`) loads only that directory, without the built-in tools. Tools are read once at start-up. After installing a package, restart the process; after adding YAML files, call `reloadTools()` / `reload()`. ### Provider packages ```bash npm install @matimo/github @matimo/slack # TypeScript pip install matimo-github matimo-slack # Python ``` Tool names come from each package's YAML, and the packages use both styles: | Package | Example tools | |---------|---------------| | `@matimo/github` | `github-create-issue`, `github-get-repository`, `github-list-repositories` | | `@matimo/gmail` | `gmail-send-email`, `gmail-list-messages`, `gmail-get-message` | | `@matimo/slack` | `slack-send-message`, `slack_send_channel_message`, `slack_get_channel_history` | | `@matimo/microsoft` | `ms_get_email`, `ms_send_email`, `ms_list_files` | Use `startsWith('slack')` rather than `'slack_'` or `'slack-'` to catch both styles. --- ## Get One Tool ```typescript const tool = matimo.getTool('calculator'); if (!tool) throw new Error('calculator is not loaded'); console.log(tool.description); console.log(Object.keys(tool.parameters ?? {})); // ['operation', 'a', 'b', 'expression', 'precision'] console.log(tool.execution.type); // 'function' ``` ```python tool = matimo.get_tool("calculator") ``` ## Search by Text `searchTools(query)` matches the query against tool names, descriptions and tags: ```typescript matimo.searchTools('email').map((t) => t.name); // ['gmail-create-draft', 'gmail-send-email', 'mailchimp-add-list-member', …, 'ms_get_email', …] ``` ```python [t.name for t in matimo.search_tools("email")] ``` ## Filter by Tag ```typescript matimo.getToolsByTag('math').map((t) => t.name); // ['calculator'] ``` Python has no `get_tools_by_tag` yet; filter the list: ```python math_tools = [t for t in matimo.list_tools() if "math" in (t.tags or [])] ``` Both `searchTools` and `getToolsByTag` accept an optional `PolicyContext` as a second argument and then return only the tools that caller may run. ## Filter by Definition ```typescript const tools = matimo.listTools(); const httpTools = tools.filter((t) => t.execution.type === 'http'); const functionTools = tools.filter((t) => t.execution.type === 'function'); const oauth2Tools = tools.filter((t) => t.authentication?.type === 'oauth2'); const noAuthTools = tools.filter((t) => !t.authentication); const needsApproval = tools.filter((t) => t.requires_approval === true); const githubTools = tools.filter((t) => t.name.startsWith('github')); ``` To list the credentials a tool needs (TypeScript): `matimo.getRequiredCredentials('github-create-issue')` → `['GITHUB_TOKEN']`. ## Tool Metadata ```typescript const tool = matimo.getTool('github-create-issue')!; tool.name; // 'github-create-issue' tool.description; // 'Create a new issue in a repository' tool.parameters; // { owner, repo, title, body, … } tool.execution; // { type: 'http', method: 'POST', url: …, headers: …, body: … } tool.authentication; // { type: 'bearer', location: 'header' } tool.requires_approval; // true tool.tags; // tags from the YAML, if any ``` ## Giving an Agent the Right Tools A model works best with a short list, and OpenAI rejects more than 128 tools. Pick by provider, tag or search before converting: ```typescript const picked = matimo.searchTools('issue').slice(0, 20); const tools = await convertToolsToLangChain(picked, matimo); ``` Agents can also search for themselves with the `matimo_search_tools` and `matimo_get_tool` meta-tools, and load guidance with the skill meta-tools; see [META_TOOLS.md](https://matimo.dev/docs/api-reference/meta-tools.md). ## Next Steps - [SDK Patterns](https://matimo.dev/docs/user-guide/sdk-patterns.md): factory, decorator and LangChain usage - [Tool Specification](https://matimo.dev/docs/tool-development/tool-specification.md): the fields you can filter on - [Authentication](https://matimo.dev/docs/user-guide/authentication.md): credentials per provider --- # LangChain Integration Source: https://matimo.dev/docs/framework-integrations/langchain/ 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](#python-langchain-integration) - [TypeScript LangChain Integration](#typescript-langchain-integration) --- ## Python LangChain Integration ### Installation ```bash pip install "matimo[langchain]" langchain-openai langchain # or with uv uv add "matimo[langchain]" langchain-openai langchain ``` ### Basic Setup ```python import asyncio import os from 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: > ```python > # Only Slack tools > slack_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 tools > relevant = matimo.search_tools('send message') > lc_tools = convert_tools_to_langchain(relevant[:20], matimo, credentials={...}) > ``` ### Complete ReAct Agent Example ```python import asyncio import os from matimo import ApprovalRequest, Matimo, convert_tools_to_langchain from langchain_openai import ChatOpenAI from 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) For full control over the tool-call loop: ```python import asyncio import json import os from matimo import Matimo, convert_tools_to_langchain from langchain_openai import ChatOpenAI from 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` ```python 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) 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). ```python # 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 automatically ``` ### 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: ```python 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) When running LangChain without an MCP server, use the skills helpers to implement progressive skill disclosure: ```python from matimo import Matimo, SkillRegistry, convert_tools_to_langchain from langchain_openai import ChatOpenAI from 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) 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: "`, and the agent can explain it or try something else. Only the model call itself can raise out of `ainvoke()`: ```python 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) ```python import os # Set tokens in environment before initialising Matimo os.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 executor ``` ### Working Examples See [python/examples/langchain/](https://github.com/tallclub/matimo/blob/main/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: ```bash cd python/examples cp .env.example .env # fill in API keys uv 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) **Tool not found:** ```python tools = matimo.list_tools() print([t.name for t in tools]) # Check available tool names ``` **Missing langchain-core:** ```bash pip install "matimo[langchain]" ``` **OAuth token missing:** ```bash export SLACK_BOT_TOKEN=xoxb-... ``` --- ## TypeScript LangChain Integration ## 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 ```bash npm install matimo langchain @langchain/core # or pnpm add matimo langchain @langchain/core ``` ## The Simplified Approach: `convertToolsToLangChain` ### 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 ```typescript 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 callback const matimo = await MatimoInstance.init({ autoDiscover: true, onApproval: async (request) => confirmWithUser(request), }); // 2. Convert the tools this agent needs const langchainTools = await convertToolsToLangChain( matimo.listTools().filter((t) => t.name.startsWith('slack')), matimo, { SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN! } ); // 3. Create the agent const agent = createAgent({ model: new ChatOpenAI({ model: 'gpt-4o-mini' }), tools: langchainTools, }); // Run it const 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: > ```typescript > const slackTools = matimo.listTools().filter(t => t.name.startsWith('slack')); > const langchainTools = await convertToolsToLangChain(slackTools, matimo, secrets); > ``` ### Complete LangChain Agent Example ```typescript 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` ```typescript export async function convertToolsToLangChain( tools: ToolDefinition[], matimo: MatimoInstance, secrets?: Record ): Promise; ``` ### Parameters - **`tools`**: Array of Matimo tool definitions to convert - **`matimo`**: MatimoInstance for tool execution - **`secrets`** _(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`, or `PASSWORD` are automatically treated as secrets ### Returns Array of LangChain-compatible tools ready for agents. ## Secret Handling ### Explicit Secret Injection ```typescript 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 **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: 1. It's **excluded from the LangChain schema** (users don't need to provide it) 2. It's **injected automatically** when present in the secrets map 3. 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., `ApiKey` matches the `KEY` pattern) **Example:** ```typescript // 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 automatically await tools[0].invoke({ channel: '#general' }); ``` ## Working Examples See [examples/tools/](https://github.com/tallclub/matimo/blob/main/typescript/examples/tools/) for complete examples: - `gmail-langchain.ts` - Gmail tool integration with LangChain - `gmail-decorator.ts` - Decorator pattern example - `gmail-factory.ts` - Factory pattern example Run them: ```bash cd examples/tools pnpm install pnpm gmail:langchain --email:your@email.com ``` ## Tool Parameter Mapping Matimo parameters map directly to LangChain function calls: ```yaml # Matimo tool definition parameters: email: type: string required: true subject: type: string required: true body: type: string required: true ``` Becomes in LangChain: ```json { "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 Tools requiring OAuth2 authentication: ```typescript // Set OAuth tokens as environment variables process.env.GMAIL_ACCESS_TOKEN = 'your-access-token'; process.env.GITHUB_TOKEN = 'your-github-token'; // Matimo automatically injects tokens into tools const result = await matimoInstance.execute('gmail-send-email', { to: 'user@example.com', subject: 'Hello', body: 'Message', // Token is automatically included from environment }); ``` ## 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: "`, 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): ```typescript 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 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: ```typescript 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](https://matimo.dev/docs/api-reference/approval-system.md). ## 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](https://matimo.dev/docs/skills/skills.md) 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 ```typescript 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 content const 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? The [agentskills.io specification](https://agentskills.io) 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 - `buildRelevantSkillPrompt` uses 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 ```typescript 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 ```typescript 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; ``` 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 **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 ### Tool Not Found Error ``` Error: Tool not found: gmail-send-email ``` **Solution**: Verify tools are loaded correctly ```typescript const tools = matimoInstance.listTools(); console.log( 'Available tools:', tools.map((t) => t.name) ); ``` ### OAuth Token Missing ``` Error: Missing OAuth token for provider: google ``` **Solution**: Set environment variable ```bash export GMAIL_ACCESS_TOKEN=your_token_here ``` ### Type Errors with LangChain Tools Ensure all Matimo tools are properly typed: ```bash pnpm validate-tools # Validates all YAML definitions ``` See [Troubleshooting Guide](https://matimo.dev/docs/troubleshooting/faq.md) for more help. --- # CrewAI Integration Source: https://matimo.dev/docs/framework-integrations/crewai/ Matimo integrates seamlessly with **CrewAI** for multi-agent orchestration. Convert any Matimo tool to a CrewAI `BaseTool` with a single function call. - [Installation](#installation) - [Basic Setup](#basic-setup) - [Single Agent Example](#single-agent-example) - [Multi-Agent Crew Example](#multi-agent-crew-example) - [Advanced Patterns](#advanced-patterns) - [Troubleshooting](#troubleshooting) --- ## Installation ### Requirements - Python 3.11+ - CrewAI >= 0.80 - Matimo >= 0.2.0 (the approval callbacks shown here are new in 0.2.0) ### Install ```bash # Install matimo with CrewAI support pip install "matimo[crewai]" matimo-slack # or with uv uv add "matimo[crewai]" matimo-slack # Install CrewAI pip install crewai ``` --- ## Basic Setup ```python import asyncio import os from matimo import Matimo from matimo.integrations.crewai import convert_tools_to_crewai from crewai import Agent, Task, Crew from langchain_openai import ChatOpenAI async def setup(): # 1. Initialize Matimo matimo = await Matimo.init(auto_discover=True) # 2. Filter tools (optional: use all or subset) slack_tools_def = [ t for t in matimo.list_tools() if t.name.startswith('slack_') ] # 3. Convert to CrewAI BaseTool crewai_tools = convert_tools_to_crewai( slack_tools_def, matimo, credentials={'SLACK_BOT_TOKEN': os.environ['SLACK_BOT_TOKEN']}, ) return matimo, crewai_tools asyncio.run(setup()) ``` --- ## Approvals and Unset Parameters A crew's tool 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, and are refused without one: ```python 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) crewai_tools = convert_tools_to_crewai(matimo.list_tools(), matimo) ``` CrewAI fills optional parameters the model leaves out with `None`; since 0.2.0 those are dropped before the call, so the tool's own defaults apply. ## Single Agent Example ```python import asyncio import os from matimo import Matimo from matimo.integrations.crewai import convert_tools_to_crewai from crewai import Agent, Task, Crew from langchain_openai import ChatOpenAI async def slack_agent_demo(): # Initialize matimo = await Matimo.init(auto_discover=True) # Get Slack tools only slack_tools_def = [ t for t in matimo.list_tools() if t.name.startswith('slack_') ] # Convert to CrewAI tools = convert_tools_to_crewai( slack_tools_def, matimo, credentials={'SLACK_BOT_TOKEN': os.environ['SLACK_BOT_TOKEN']}, ) # Create agent slack_manager = Agent( role='Slack Manager', goal='Send messages and manage Slack channels', tools=tools, llm=ChatOpenAI(model='gpt-4o-mini'), verbose=True, ) # Create task task = Task( description='Send a greeting message to #general channel', agent=slack_manager, expected_output='Message sent successfully', ) # Run crew (actually runs the agent) crew = Crew(agents=[slack_manager], tasks=[task]) result = crew.kickoff() print(f"Result: {result}") # Run asyncio.run(slack_agent_demo()) ``` --- ## Multi-Agent Crew Example ```python import asyncio import os from matimo import Matimo from matimo.integrations.crewai import convert_tools_to_crewai from crewai import Agent, Task, Crew from langchain_openai import ChatOpenAI async def multi_agent_crew(): # Initialize Matimo matimo = await Matimo.init(auto_discover=True) # Get tools by provider slack_tools = [ t for t in matimo.list_tools() if t.name.startswith('slack_') ] github_tools = [ t for t in matimo.list_tools() if t.name.startswith('github_') ] # Convert both slack_crewai_tools = convert_tools_to_crewai( slack_tools, matimo, credentials={'SLACK_BOT_TOKEN': os.environ['SLACK_BOT_TOKEN']}, ) github_crewai_tools = convert_tools_to_crewai( github_tools, matimo, credentials={'GITHUB_TOKEN': os.environ['GITHUB_TOKEN']}, ) # Create agents slack_manager = Agent( role='Slack Communicator', goal='Handle all Slack communications', tools=slack_crewai_tools, llm=ChatOpenAI(model='gpt-4o-mini'), ) github_manager = Agent( role='GitHub Coordinator', goal='Manage GitHub repositories and issues', tools=github_crewai_tools, llm=ChatOpenAI(model='gpt-4o-mini'), ) # Create tasks slack_task = Task( description='Post daily standup to #engineering', agent=slack_manager, expected_output='Standup posted', ) github_task = Task( description='List open issues for the project', agent=github_manager, expected_output='Issues listed', ) # Run crew with multiple agents crew = Crew( agents=[slack_manager, github_manager], tasks=[slack_task, github_task], verbose=True, ) result = crew.kickoff() print(f"Crew result: {result}") asyncio.run(multi_agent_crew()) ``` --- ## Advanced Patterns ### Pattern 1: Hierarchical Crew (Manager Pattern) ```python from crewai import Agent, Task, Crew async def hierarchical_crew(): matimo = await Matimo.init(auto_discover=True) # Worker agents slack_worker = Agent( role='Slack Specialist', goal='Execute Slack operations', tools=convert_tools_to_crewai([...], matimo), ) github_worker = Agent( role='GitHub Specialist', goal='Execute GitHub operations', tools=convert_tools_to_crewai([...], matimo), ) # Manager agent coordinates workers manager = Agent( role='Project Manager', goal='Coordinate team activities', tools=[], # Manager uses reports, not direct tools ) # Tasks with dependencies tasks = [ Task(description='Check GitHub issues', agent=github_worker), Task(description='Post status to Slack', agent=slack_worker), Task(description='Summarize progress', agent=manager), ] crew = Crew(agents=[slack_worker, github_worker, manager], tasks=tasks) result = crew.kickoff() return result asyncio.run(hierarchical_crew()) ``` ### Pattern 2: Sequential vs Parallel Execution ```python from crewai import Crew, Process # Sequential (default): one task at a time crew_sequential = Crew( agents=[agent1, agent2], tasks=[task1, task2], # task2 waits for task1 process=Process.sequential, # Default ) # Hierarchical: manager coordinates workers crew_hierarchical = Crew( agents=[worker1, worker2, manager], tasks=[task1, task2, task3], process=Process.hierarchical, manager_agent=manager, ) result = crew_sequential.kickoff() ``` ### Pattern 3: Custom Credentials Per Agent ```python import os async def custom_credentials(): matimo = await Matimo.init(auto_discover=True) # Different credentials for different agents agent1_tools = convert_tools_to_crewai( tools, matimo, credentials={ 'SLACK_BOT_TOKEN': os.environ.get('SLACK_BOT_TOKEN_PROD'), } ) agent2_tools = convert_tools_to_crewai( tools, matimo, credentials={ 'SLACK_BOT_TOKEN': os.environ.get('SLACK_BOT_TOKEN_DEV'), } ) # Agents use isolated credentials agent1 = Agent(..., tools=agent1_tools) agent2 = Agent(..., tools=agent2_tools) ``` --- ## Troubleshooting ### Issue: Tools Not Showing in Agent **Problem**: Agent can't see/use tools in CrewAI. **Solution**: Verify tools are converted properly: ```python tools = convert_tools_to_crewai(tool_definitions, matimo) print(f"Converted {len(tools)} tools") print([t.name for t in tools]) # Should see tool names ``` ### Issue: Credentials Not Working **Problem**: Tool execution fails with authentication error. **Solution**: Pass credentials at conversion time: ```python tools = convert_tools_to_crewai( tool_definitions, matimo, credentials={'SLACK_BOT_TOKEN': os.environ['SLACK_BOT_TOKEN']}, ) ``` ### Issue: Performance/Timeout **Problem**: Tools run slow or timeout in Jupyter/async context. **Solution**: CrewAI uses a shared executor for event loops; ensure Jupyter environment has async support: ```python # In Jupyter, use asyncio properly import asyncio import nest_asyncio nest_asyncio.apply() # Allow nested event loops ``` --- ## Best Practices 1. **Filter tools by provider**: Don't load all 153 native tools; be specific ```python slack_tools = [t for t in matimo.list_tools() if 'slack' in t.name] ``` 2. **Pass credentials at conversion**: Isolate credentials per agent ```python tools = convert_tools_to_crewai(slack_tools, matimo, credentials={...}) ``` 3. **Use descriptive agent roles**: Help LLM understand tool purpose ```python Agent(role='Slack Message Sender', goal='Send messages to Slack', ...) ``` 4. **Set verbose=True for debugging**: See what the agent is doing ```python crew = Crew(..., verbose=True) ``` 5. **Monitor tool coverage**: Ensure agent has the tools it needs ```python print(f"Agent has {len(agent.tools)} tools") ``` --- ## See Also - [LangChain Integration](https://matimo.dev/docs/framework-integrations/langchain.md) - [Agno Integration](https://matimo.dev/docs/framework-integrations/agno.md): Use with LangChain agents - [Native Python Patterns](https://matimo.dev/docs/user-guide/sdk-patterns.md#python-sdk): SDK without framework - [Tool Discovery](https://matimo.dev/docs/user-guide/tool-discovery.md): Provider packages and their tools - [API Reference](https://matimo.dev/docs/api-reference/sdk.md#python-sdk-matimo-api): Python SDK API --- # Agno Integration Source: https://matimo.dev/docs/framework-integrations/agno/ Matimo integrates with **[Agno](https://docs.agno.com)** for agents, teams and workflows. Convert any Matimo tool to an Agno `Function`, or hand an agent a whole governed toolkit with one call. The difference from a plain tool list: Agno keeps deciding *which* tool to call, and Matimo keeps deciding *whether* it may run. A tool that Matimo would ask a human about arrives in Agno with `requires_confirmation` already set, so the run pauses for a human without you writing any policy code, and the human's answer is the approval: Matimo does not ask again. - [Installation](#installation) - [Basic Setup](#basic-setup) - [Risk to Confirmation Mapping](#risk-to-confirmation-mapping) - [Single Agent Example](#single-agent-example) - [Confirmation Flow](#confirmation-flow) - [API Reference](#api-reference) - [Secret Handling](#secret-handling) - [Skills Integration](#skills-integration) - [Error Handling](#error-handling) - [Advanced Patterns](#advanced-patterns) - [Non-Python Runtimes](#non-python-runtimes) - [Troubleshooting](#troubleshooting) - [Working Examples](#working-examples) > Agno is a Python SDK, so this integration is Python only. There is no Agno TypeScript SDK to target. If you need governed Matimo tools from a non-Python runtime, see [Non-Python Runtimes](#non-python-runtimes). --- ## Installation ### Requirements - Python 3.11+ - Agno >= 3.0 - Matimo >= 0.2.0 (the Agno integration is new in 0.2.0) ### Install ```bash # Install matimo with Agno support pip install "matimo[agno]" matimo-slack # or with uv uv add "matimo[agno]" matimo-slack # Install Agno plus a model provider pip install agno openai ``` --- ## Basic Setup ```python import asyncio from agno.agent import Agent from agno.models.openai import OpenAIChat from matimo import Matimo from matimo.integrations.agno import MatimoTools async def setup(): # 1. Initialize Matimo. The policy engine is active from here on. matimo = await Matimo.init(auto_discover=True) # 2. Filter to the tools this agent should see. slack_tools = [t for t in matimo.list_tools() if t.name.startswith("slack")] # 3. Hand them to Agno as a toolkit. agent = Agent( model=OpenAIChat(id="gpt-5.4"), tools=[MatimoTools(matimo, slack_tools)], ) await agent.aprint_response("Post a hello message to the first public channel") asyncio.run(setup()) ``` `MatimoTools` is named to match Agno's own `Tools` convention, so it sits alongside `SlackTools()` or `YFinanceTools()` in the `tools=[...]` list. --- ## Risk to Confirmation Mapping A tool arrives in Agno with `requires_confirmation=True` when either of these holds: 1. **Its definition needs approval on every call:** it declares `requires_approval: true`, or it is an HTTP `DELETE` or `type: command` tool without `requires_approval: false` (the 0.2.0 default in `secure` governance mode; `legacy` mode has no such default). 2. **Its execution risk is in `confirm_risk_levels`** (default `high` and `critical`), as computed by `classify_execution_risk()`: | Execution risk | How it is reached | Agno behavior by default | |---|---|---| | `low` | HTTP GET, or a function tool declaring `risk: low` | Runs immediately | | `medium` | HTTP POST, PUT, PATCH, or a function tool declaring `risk: medium` | Runs immediately | | `high` | HTTP DELETE, an HTTP tool with `requires_approval: true`, `type: command`, a function tool declaring `risk: high` or no `risk:`, or a function tool with `requires_approval: true` | `requires_confirmation=True`, run pauses | | `critical` | A tool declaring `risk: critical` | `requires_confirmation=True`, run pauses | A declared `risk:` can only raise the level computed for HTTP and command tools, never lower it. For a `type: function` tool the declared risk is the execution risk, because every function tool in the registry is developer-authored. A call the human confirms in Agno reaches `Matimo.execute()` with `approved=True`, so Matimo's own approval prompt is skipped. The policy engine and HITL quarantine still run. A call that pauses nothing in Agno but still needs Matimo's approval (for example a `postgres-execute-sql` call whose SQL contains `DELETE`) goes to the `on_approval` callback passed to `Matimo.init()`; with no callback it is rejected. Change the set with `confirm_risk_levels`: ```python # Also pause on writes MatimoTools(matimo, tools, confirm_risk_levels=["medium", "high", "critical"]) # Never pause in Agno; every approval goes to Matimo's on_approval callback instead MatimoTools(matimo, tools, confirm_risk_levels=[]) ``` --- ## Single Agent Example A stock desk agent with one read tool and one write tool: ```python import asyncio import os from agno.agent import Agent from agno.models.openai import OpenAIChat from matimo import Matimo from matimo.integrations.agno import MatimoTools async def run(task: str) -> None: matimo = await Matimo.init("./tools", log_level="silent") toolkit = MatimoTools(matimo, matimo.list_tools(), name="stock_tools") agent = Agent( model=OpenAIChat(id=os.environ["OPENAI_MODEL"]), tools=[toolkit], instructions="You are a stock market assistant. Report concrete numbers.", ) result = await agent.arun(task) print(result.content) asyncio.run(run("What is the latest quote for AAPL?")) ``` --- ## Confirmation Flow When the model calls a tool that needs confirmation, the run pauses instead of executing. Resolve the requirement, then continue: ```python result = await agent.arun("Buy 5 shares of AAPL") if result.is_paused: for requirement in result.active_requirements: if requirement.needs_confirmation: call = requirement.tool_execution print(f"Approve {call.tool_name}({call.tool_args})?") if input("[y/N] ").strip().lower() == "y": requirement.confirm() else: requirement.reject() result = await agent.acontinue_run(run_response=result) print(result.content) ``` A rejected requirement means the tool never executes, and Matimo never sees the call. A confirmed one runs as approved, so the human is asked once. --- ## API Reference ### MatimoTools ```python MatimoTools( matimo: Matimo, tools: list[ToolDefinition] | None = None, *, credentials: dict[str, str] | None = None, context: PolicyContext | None = None, confirm_risk_levels: Sequence[RiskLevel | str] | None = None, name: str = "matimo_tools", instructions: str | None = None, add_instructions: bool = True, **toolkit_kwargs, ) -> Toolkit ``` | Parameter | Type | Description | |---|---|---| | `matimo` | `Matimo` | Async instance used to execute the tools | | `tools` | `list[ToolDefinition] \| None` | Tools to expose. Defaults to `matimo.list_tools()` | | `credentials` | `dict[str, str] \| None` | Per-call credential overrides, keyed by the placeholder names the YAML references (for example `SLACK_BOT_TOKEN`), not by tool parameter name | | `context` | `PolicyContext \| None` | Agent id, environment and roles, forwarded to the policy engine on every call | | `confirm_risk_levels` | `Sequence[RiskLevel \| str] \| None` | Execution risk levels that pause the run, on top of tools whose definition needs approval on every call. Defaults to `("high", "critical")`; empty disables Agno-side confirmation | | `name` | `str` | Toolkit name shown in Agno logs | | `instructions` | `str \| None` | Usage guidance added to the agent's context | | `add_instructions` | `bool` | Whether to add `instructions` to the agent context | | `**toolkit_kwargs` | | Forwarded to Agno's `Toolkit`, for example `include_tools`, `exclude_tools`, `cache_results` | **Returns:** an Agno `Toolkit` with both a sync and an async variant of each tool registered, so `agent.run()` and `agent.arun()` are each served natively. **Raises:** `ImportError` if agno is not installed. ### convert_tools_to_agno ```python convert_tools_to_agno( tools: list[ToolDefinition], matimo: Matimo, credentials: dict[str, str] | None = None, *, context: PolicyContext | None = None, confirm_risk_levels: Sequence[RiskLevel | str] | None = None, ) -> list[Function] ``` The lower-level call, matching `convert_tools_to_langchain` and `convert_tools_to_crewai`. Returns a list of Agno `Function` objects for `Agent(tools=[...])`. **Raises:** `ImportError` if agno is not installed. Both are also importable from the top level: ```python from matimo import MatimoTools, convert_tools_to_agno ``` --- ## Secret Handling Secret parameters are excluded from the schema sent to the model, so the LLM never sees a credential. A parameter counts as a secret when its name contains `TOKEN`, `KEY`, `SECRET` or `PASSWORD`, case insensitive. Values are injected at execution time from the `credentials` argument or the environment. ```python # The model sees only `channel` and `text`; SLACK_BOT_TOKEN is injected at call time. MatimoTools(matimo, slack_tools, credentials={"SLACK_BOT_TOKEN": os.environ["SLACK_BOT_TOKEN"]}) ``` --- ## Skills Integration Matimo skills are domain knowledge, separate from tools. Pass a skill's content as the toolkit's instructions so the agent gets it as context: ```python toolkit = MatimoTools( matimo, slack_tools, instructions=matimo.get_skill_content("slack"), ) ``` For token-efficient loading, rank skills against the current request first and inline only what matches: ```python from matimo import build_relevant_skill_prompt guidance = await build_relevant_skill_prompt(matimo, user_message, top_k=2) toolkit = MatimoTools(matimo, tools, instructions=guidance) ``` See [Skills System](https://matimo.dev/docs/skills/skills.md) for the progressive disclosure model. --- ## Error Handling Tool errors propagate as `MatimoError`, which carries a machine-readable code: ```python from matimo.errors import ErrorCode, MatimoError try: result = await agent.arun("Delete the production database") except MatimoError as exc: if exc.code is ErrorCode.POLICY_DENIED: print(f"Blocked by policy: {exc}") elif exc.code is ErrorCode.TOOL_NOT_FOUND: print(f"No such tool: {exc.details.get('tool_name')}") else: raise ``` `ErrorCode.POLICY_DENIED` covers both an outright policy denial and a quarantined tool whose Matimo-side HITL approval was refused or timed out. --- ## Advanced Patterns ### Pattern 1: Role-based access with PolicyContext The same toolkit behaves differently per caller. In production, a tool declaring `requires_approval: true` is denied unless the caller holds the `admin` or `operator` role: ```python from matimo.core.models import PolicyContext analyst = PolicyContext(agent_id="desk-bot", environment="production", roles=["analyst"]) operator = PolicyContext(agent_id="desk-bot", environment="production", roles=["operator"]) read_only_agent = Agent(model=model, tools=[MatimoTools(matimo, tools, context=analyst)]) trading_agent = Agent(model=model, tools=[MatimoTools(matimo, tools, context=operator)]) ``` ### Pattern 2: Choosing which tools to advertise ```python # Everything, including tools that will pause for approval MatimoTools(matimo, matimo.list_tools()) # Only tools that need no approval for this caller. # Note this also excludes quarantined tools, which are usable once approved. MatimoTools(matimo, matimo.get_tools_for_agent(context)) ``` ### Pattern 3: Staying under a model's tool limit OpenAI accepts at most 128 tools per request. With `auto_discover=True` across many providers you can exceed that, so filter first: ```python allowed_prefixes = ("slack_", "github_", "read", "search", "web") tools = [t for t in matimo.list_tools() if t.name.startswith(allowed_prefixes)][:128] MatimoTools(matimo, tools) ``` --- ## Non-Python Runtimes Agno is a Python SDK, so there is no TypeScript connector. Agno does consume Model Context Protocol servers, and Matimo ships one, so the same governed tools reach an Agno agent over MCP: ```bash # Start Matimo's MCP server matimo mcp # stdio matimo mcp --transport http --port 3000 # HTTP ``` ```python from agno.agent import Agent from agno.tools.mcp import MCPTools async with MCPTools(command="matimo mcp") as matimo_mcp: agent = Agent(model=model, tools=[matimo_mcp]) await agent.aprint_response("List the available Slack channels") ``` The policy engine still gates every call, since the MCP server executes through the same `Matimo.execute()` path. The tradeoff versus the native connector is an extra process hop and no direct access to `PolicyContext` per call. Prefer `MatimoTools` when your agent is Python. See [MCP Server](https://matimo.dev/docs/mcp-server.md) for transports and client configuration. --- ## Troubleshooting **`ImportError: agno is required for the Agno integration`:** install the extra with `pip install "matimo[agno]"`. **The run never pauses on a destructive tool:** check the tool's execution risk and whether its definition needs approval. Only `high` and `critical` pause by default, and HTTP POST classifies as `medium`. Declare `requires_approval: true` or `risk: high` in the YAML, or pass `confirm_risk_levels=["medium", "high", "critical"]`. ```python from matimo import classify_execution_risk, definition_requires_approval tool = matimo.get_tool("my_tool") print(classify_execution_risk(tool).value, definition_requires_approval(tool)) ``` **`Destructive operation requires approval` after the run was not paused:** the call needed Matimo's approval for a reason Agno could not see in advance, such as a destructive keyword in its SQL or command. Pass `on_approval=` to `Matimo.init()`, or pre-approve the tool with `MATIMO_APPROVED_PATTERNS`. **The model says a tool takes no arguments:** do not register Matimo tools through Agno's `async_tools=[(callable, name)]` parameter. Agno derives a schema from the callable's signature, and these entrypoints take `**kwargs`, which yields an empty parameter list. `MatimoTools` handles this correctly. **`400 Function tools with reasoning_effort are not supported ... in /v1/chat/completions`:** the model is a reasoning model, and Agno's `OpenAIChat` posts to `/v1/chat/completions`. Use `OpenAIResponses`, which posts to `/v1/responses`: ```python from agno.models.openai import OpenAIResponses agent = Agent(model=OpenAIResponses(id="gpt-5.6"), tools=[MatimoTools(matimo)]) ``` **`401 invalid_api_key` even though the key in `.env` is good:** `load_dotenv()` does not override variables already present in the environment, so a stale `OPENAI_API_KEY` in your shell wins. Pass `override=True`: ```python load_dotenv(Path(__file__).parent / ".env", override=True) ``` **Credentials not reaching the API:** keys in `credentials` must match the placeholder names in the YAML (`SLACK_BOT_TOKEN`), not the tool's parameter names. An unresolved placeholder raises `ErrorCode.INVALID_PARAMETER`. **Tool call fails only under `agent.run()`:** the sync entrypoint drives Matimo's async `execute()` through a worker thread when a loop is already running. If you are inside an existing event loop, prefer `agent.arun()`, which reaches `execute()` directly. --- ## Working Examples See [python/examples/agno/](https://github.com/tallclub/matimo/blob/main/python/examples/agno/). | File | Description | |---|---| | `agents/agno_agent.py` | Multi-provider Agno agent over auto-discovered tools | | `agents/agno_with_approval.py` | Confirmation flow driven by risk classification | ```bash cd python/examples cp .env.example .env uv run python agno/agents/agno_agent.py ``` --- ## See Also - [LangChain Integration](https://matimo.dev/docs/framework-integrations/langchain.md): ReAct agents, Python and TypeScript - [CrewAI Integration](https://matimo.dev/docs/framework-integrations/crewai.md): multi-agent crews, Python - [Policy Engine and Tool Lifecycle](https://matimo.dev/docs/api-reference/policy-and-lifecycle.md): risk classification and HITL - [SDK Usage Patterns](https://matimo.dev/docs/user-guide/sdk-patterns.md#python-sdk): factory and decorator patterns - [MCP Server](https://matimo.dev/docs/mcp-server.md): governed tools over Model Context Protocol --- # Composio Integration Source: https://matimo.dev/docs/composio/ # @matimo/composio: Governed Access to Composio's Tool Catalog `@matimo/composio` turns [Composio](https://composio.dev)'s 250+ integrations from "agent has raw API access" into "agent has governed, auditable, human-approvable API access", same breadth, with Matimo's policy engine, risk classification, and human-in-the-loop (HITL) approval layered on top. > **TypeScript only.** `@matimo/composio` ships for the TypeScript SDK (`npm install @matimo/composio`). There is no Python package for it yet. > **Bring your own key.** Every tool here requires *your own* `COMPOSIO_API_KEY` and Composio connected accounts, supplied at runtime, Matimo does not hold a Composio account or route requests through Matimo-operated infrastructure. You're responsible for complying with [Composio's Terms of Service](https://composio.dev/terms) and [Privacy Policy](https://composio.dev/privacy) independently. Matimo is not affiliated with, endorsed by, or sponsored by Composio. See [`THIRD_PARTY_NOTICES.md`](https://github.com/tallclub/matimo/blob/main/THIRD_PARTY_NOTICES.md) for the full list of third-party providers Matimo can connect to. --- ## What Is Composio? Composio is a managed integration platform that provides pre-built connectors to hundreds of SaaS tools (Jira, Google Workspace, Microsoft 365, Salesforce, GitHub, Linear, Asana, and more). Its key primitives: | Concept | What it is | |---------|-----------| | **Toolkit** | A named group of actions for one SaaS product (e.g. `jira`, `googledrive`) | | **Action / Slug** | A single API operation (e.g. `JIRA_GET_ISSUE`), addressable by slug | | **Connected Account** | An OAuth2 token for one user+toolkit combination (e.g. Sajesh's Jira token) | | **Entity / User ID** | Your application's stable identifier for a user (passed as `composio_user_id`) | `@matimo/composio` generates one Matimo tool per Composio action, exposes them all via `MatimoInstance`, and adds risk classification + optional HITL approval before any action reaches Composio's execute endpoint. --- ## Architecture ![An agent calls matimo.execute on a Composio tool. MatimoInstance runs the policy check, the optional onHITL callback and the HTTP executor against the Composio REST API, which forwards to your connected services such as Jira, Google Drive and Teams.](../../../assets/diagrams/composio-flow-light.svg) ![An agent calls matimo.execute on a Composio tool. MatimoInstance runs the policy check, the optional onHITL callback and the HTTP executor against the Composio REST API, which forwards to your connected services such as Jira, Google Drive and Teams.](../../../assets/diagrams/composio-flow-dark.svg) Every generated tool is `type: http`, no custom executor code. `@matimo/composio` stays in `typescript/packages/composio/` as a managed third-party integration (intentionally separate from the root `packages/` workspace). --- ## Available Toolkits | Toolkit slug | Matimo prefix | Tools | Examples | |---|---|---|---| | `jira` | `composio_jira_*` | 46 | `get_issue`, `create_issue`, `search_issues` | | `asana` | `composio_asana_*` | 84 | `get_task`, `create_task`, `update_task` | | `linear` | `composio_linear_*` | 21 | `get_issue`, `create_linear_issue`, `archive_issue` | | `googledrive` | `composio_googledrive_*` | 51 | `list_files`, `find_file`, `upload_file` | | `googlecalendar` | `composio_googlecalendar_*` | 28 | `list_calendars`, `create_event`, `find_event` | | `outlook` | `composio_outlook_*` | 43 | `list_emails`, `send_email`, `create_calendar` | | `one_drive` | `composio_one_drive_*` | 35 | `list_drives`, `download_file`, `search_items` | | `share_point` | `composio_share_point_*` | 6 | `create_folder`, `create_list`, `find_user` | | `microsoft_teams` | `composio_microsoft_teams_*` | 28 | `teams_list`, `teams_post_channel_message`, `create_meeting` | | `gmail` | `composio_gmail_*` | 23 | `send_email`, `fetch_emails`, `create_email_draft` | | `googlesheets` | `composio_googlesheets_*` | 36 | `batch_get`, `create_spreadsheet_row`, `create_chart` | | `googledocs` | `composio_googledocs_*` | 32 | `create_document`, `replace_all_text`, `insert_table_action` | | `googleforms` | `composio_googleforms_*` | 7 | `create_form`, `get_response`, `list_responses` | | `googlemeet` | `composio_googlemeet_*` | 9 | `create_meet`, `list_conference_records`, `get_transcripts_by_conference_record_id` | **Total: 449 tools** across 14 toolkits (as of 0.3.0). Add more toolkits by running the generator, see [Generating More Toolkits](#generating-more-toolkits). --- ## Every Tool Call Needs Three Things All 449 tools share the same three required inputs: | Input | How it's supplied | |-------|-----------------| | `COMPOSIO_API_KEY` | Environment variable, auto-injected as `x-api-key` header | | `composio_user_id` | Tool parameter, your application's stable user/tenant identifier | | `composio_connected_account_id` | Tool parameter, the OAuth connected account for *this user + this toolkit* | `composio_connected_account_id` is obtained when a user completes the Composio OAuth flow ("Connect Jira", "Connect Google Drive", etc.). Your application is responsible for storing and retrieving the correct account ID per user per toolkit, this is out of scope for `@matimo/composio`. Every tool also takes an optional `composio_toolkit_version` (default `latest`, new in 0.3.0). It is sent to Composio as `version`. Without it Composio v3 runs the stale base version `00000000_00`, which can call upstream APIs that no longer exist. Pass a dated version such as `20251027_00` to pin one. --- ## Quick Start ```typescript import path from 'path'; import { MatimoInstance } from '@matimo/core'; const TOOLS_DIR = path.join(__dirname, 'path/to/packages/composio/tools'); const matimo = await MatimoInstance.init({ toolPaths: [TOOLS_DIR] }); // Low-risk: executes immediately const user = await matimo.execute('composio_jira_get_current_user', { composio_user_id: 'user_123', composio_connected_account_id: 'ca_abc123', }); // Medium- and high-risk tools also execute immediately with the default // policy, see "Governance" below to require approval for them const files = await matimo.execute('composio_googledrive_list_files', { composio_user_id: 'user_123', composio_connected_account_id: 'ca_def456', }); ``` --- ## Risk Classification Every generated tool has an explicit `risk: low | medium | high` field derived from the Composio action name: | Pattern in action slug | Risk | Examples | |---|---|---| | `GET`, `LIST`, `FETCH`, `SEARCH`, `READ`, `FIND` | `low` | `JIRA_GET_ISSUE`, `GOOGLEDRIVE_LIST_FILES` | | `CREATE`, `SEND`, `UPDATE`, `EDIT`, `ADD`, `UPLOAD`, `INVITE` | `medium` | `JIRA_CREATE_ISSUE`, `OUTLOOK_SEND_EMAIL` | | `DELETE`, `REMOVE`, `ARCHIVE`, `REVOKE`, `CANCEL` | `high` | `JIRA_DELETE_ISSUE`, `GOOGLECALENDAR_CALENDARS_DELETE` | | Matches none | `medium` (safe default) | n/a | Destructive patterns are checked first, so `ARCHIVE_AND_GET` → `high`. Some actions are overridden manually in `scripts/risk-overrides.json` when the heuristic gets it wrong (e.g. `GOOGLEDRIVE_EMPTY_TRASH` → `high`, `GOOGLECALENDAR_CLEAR_CALENDAR` → `high`). Read the declared level from the definition. `classifyExecutionRisk(tool)`, which the policy engine uses, rates every Composio tool at least `medium`, because each one is an HTTP POST to Composio: ```typescript import { classifyExecutionRisk } from '@matimo/core'; const del = matimo.getTool('composio_jira_delete_issue'); console.log(del?.risk, classifyExecutionRisk(del!)); // 'high' 'high' const get = matimo.getTool('composio_jira_get_current_user'); console.log(get?.risk, classifyExecutionRisk(get!)); // 'low' 'medium' ``` --- ## Governance: Adding HITL Approval **By default nothing asks.** Generated Composio tools are HTTP POSTs and declare no `requires_approval`, so 0.2.0's per-call approval default (HTTP DELETE and command tools) does not cover them: even a `risk: high` action such as `composio_jira_delete_issue` runs as soon as it is called. Turn on one of the two options below before giving an agent write access. **Quarantine high-risk actions** with the default policy engine: ```typescript const matimo = await MatimoInstance.init({ toolPaths: [TOOLS_DIR], policyConfig: { enableHITL: true, hitlMinRiskLevel: 'high' }, onHITL: async (request) => { console.log(`Approval needed: ${request.toolName} (${request.riskLevel})`); return promptUser(); // Promise }, }); ``` This pauses the 45 `risk: high` Composio tools, and every other high or critical tool in the instance (native DELETE tools, `execute`, `edit`, …). A lower threshold cannot separate Composio reads from writes, since the engine rates every Composio POST at least `medium`. **Quarantine by declared risk** with a small custom engine, to also pause `medium` writes while `low` reads run immediately: ```typescript import { MatimoInstance, DefaultPolicyEngine, type PolicyEngine, type PolicyContext, type PolicyDecision, type RiskLevel, type ToolDefinition, } from '@matimo/core'; const QUARANTINE_LEVELS: ReadonlySet = new Set(['medium', 'high']); class ComposioRiskPolicy implements PolicyEngine { private base = new DefaultPolicyEngine(); canExecute(ctx: PolicyContext, tool: ToolDefinition): PolicyDecision { const base = this.base.canExecute(ctx, tool); if (base.allowed !== true) return base; if (tool.name.startsWith('composio_') && tool.risk && QUARANTINE_LEVELS.has(tool.risk)) { return { allowed: 'pending_approval', riskLevel: tool.risk, reason: `${tool.name} (risk: ${tool.risk}) requires approval`, toolName: tool.name, }; } return { allowed: true }; } canCreate(ctx: PolicyContext, tool: ToolDefinition): PolicyDecision { return this.base.canCreate(ctx, tool); } filterForAgent(ctx: PolicyContext, tools: ToolDefinition[]): ToolDefinition[] { return this.base.filterForAgent(ctx, tools); } } const matimo = await MatimoInstance.init({ toolPaths: [TOOLS_DIR], policy: new ComposioRiskPolicy(), onHITL: async (request) => { // Wire to: interactive prompt, Slack message, approval queue, etc. console.log(`Approval needed: ${request.toolName} (${request.riskLevel})`); return promptUser(); // Promise }, }); ``` With no `onHITL`, a quarantined call is rejected. Approvals and rejections reach `onEvent` and the `auditSink` as `tool:quarantine_approved` / `tool:quarantine_rejected`. See `typescript/examples/tools/composio/composio-with-approval.ts` for the full runnable example. --- ## LangChain Integration With 449 tools, loading all of them into a LangChain agent exceeds OpenAI's 128-tool limit. **Always filter to the toolkits relevant to the agent before binding:** ```typescript import { MatimoInstance, convertToolsToLangChain, type ToolDefinition } from '@matimo/core'; import { ChatOpenAI } from '@langchain/openai'; import { createAgent } from 'langchain'; const matimo = await MatimoInstance.init({ toolPaths: [TOOLS_DIR] }); // Pick only the toolkits this agent needs const jiraTools = matimo.getRegistry().getAll() .filter(t => t.name.startsWith('composio_jira_')); // convertToolsToLangChain(tools, matimoInstance, envVarsToInject) // COMPOSIO_API_KEY is resolved from process.env automatically via Matimo's // auth-parameter injection. composio_user_id and composio_connected_account_id // are tool parameters, include them in the task/system prompt so the LLM // knows what values to pass on each call. const langchainTools = await convertToolsToLangChain( jiraTools as ToolDefinition[], matimo, { COMPOSIO_API_KEY: process.env.COMPOSIO_API_KEY! } ); const agent = await createAgent({ model: new ChatOpenAI({ modelName: 'gpt-4o-mini' }), tools: langchainTools as any[] }); const response = await agent.invoke({ messages: [{ role: 'user', content: 'Find all unresolved issues assigned to me. composio_user_id="user_123", composio_connected_account_id="ca_abc123"' }], }); const lastMessage = response.messages[response.messages.length - 1]; console.log(lastMessage.content); ``` > **Note on Jira search actions**: As of March 2026, Jira removed the `/rest/api/3/search` endpoint. If `composio_jira_search_issues` or `composio_jira_search_for_issues_using_jql_post` return HTTP 410, Composio's catalog hasn't updated to the new `/rest/api/3/search/jql` endpoint yet. Use `composio_jira_get_issue` or `composio_jira_get_current_user` as a workaround, or wait for Composio to update the action. See `typescript/examples/tools/composio/composio-langchain.ts` for the complete example. --- ## Naming Conventions Tool names follow `composio__`. Some Composio action slugs already include the toolkit name, resulting in repetition: | Composio slug | Matimo tool name | |---|---| | `JIRA_GET_ISSUE` | `composio_jira_get_issue` | | `LINEAR_CREATE_LINEAR_ISSUE` | `composio_linear_create_linear_issue` | | `ONE_DRIVE_ONEDRIVE_CREATE_FOLDER` | `composio_one_drive_onedrive_create_folder` | | `SHARE_POINT_SHAREPOINT_CREATE_LIST` | `composio_share_point_sharepoint_create_list` | This is Composio's own naming, Matimo preserves it exactly. Always verify tool names against the `tools/composio_*/` directories rather than assuming a "natural" name. --- ## Generating More Toolkits The tool definitions under `tools/composio_*/` are **generated**, do not hand-edit them. To add a new Composio toolkit: ```bash cd typescript/ export COMPOSIO_API_KEY=your-key # Add new toolkits (idempotent: existing files are left untouched) pnpm generate:composio --toolkits=SALESFORCE,HUBSPOT,GITHUB # Re-generate a toolkit after editing risk-overrides.json pnpm generate:composio --toolkits=SALESFORCE --force-refresh # Validate all generated YAML against Matimo's schema pnpm validate-tools ``` To override a misclassified risk level, add an entry to `typescript/packages/composio/scripts/risk-overrides.json`: ```json { "GOOGLEDRIVE_EMPTY_TRASH": "high", "GOOGLECALENDAR_CLEAR_CALENDAR": "high", "SALESFORCE_BULK_DELETE_RECORDS": "high" } ``` Then re-run with `--force-refresh` for the affected toolkits. --- ## Response Structure Every composio tool returns the same envelope: ```json { "success": true, "statusCode": 200, "data": { "data": { "...action-specific payload..." }, "error": null, "successful": true } } ``` Check **`data.successful`** (not just `success`) to confirm the underlying action succeeded. A `success: true` with `data.successful: false` means Composio's HTTP layer worked but the integration itself rejected the call (e.g. invalid issue ID, expired token, insufficient scope on the connected account). --- ## Missing Connected Account If `composio_connected_account_id` is missing, invalid, or not yet connected, Composio returns a "no connected account" error. Surface this to the user as **"connect `` first"**, do not retry, as retrying cannot create the missing OAuth connection. --- ## See Also - [Examples, composio-factory.ts](https://github.com/tallclub/matimo/blob/main/typescript/examples/tools/composio/composio-factory.ts) - [Examples, composio-decorator.ts](https://github.com/tallclub/matimo/blob/main/typescript/examples/tools/composio/composio-decorator.ts) - [Examples, composio-langchain.ts](https://github.com/tallclub/matimo/blob/main/typescript/examples/tools/composio/composio-langchain.ts) - [Examples, composio-with-approval.ts](https://github.com/tallclub/matimo/blob/main/typescript/examples/tools/composio/composio-with-approval.ts) - [SKILL.md agent knowledge doc](https://github.com/tallclub/matimo/blob/main/typescript/packages/composio/skills/composio/SKILL.md) - [Composio official docs](https://docs.composio.dev) --- # Matimo OSS 0.2.0: tool governance, on by default Source: https://matimo.dev/docs/blog/2026-10-01-matimo-0-2-0-tool-governance/ Most agent frameworks make it easy to give an agent a tool. Few of them make it easy to answer the questions that come right after: Did a human approve that delete? Could the agent have approved its own tool? Can I trust the log of what ran? Matimo OSS 0.2.0 is about making those answers true, in both the TypeScript and Python SDKs. ## What changed **Risky calls now ask first.** An HTTP `DELETE` tool or a shell-command tool asks for approval on every call unless its YAML says `requires_approval: false`. With no one to answer, the call is refused, not run. Here is the real output from the no-key demo (`pnpm policy:approval-modes`): ``` 2. Secure mode (secure) with no approval callback shell_echo: Destructive operation requires approval: shell_echo 4. A per-call onApproval overrides the instance callback per-call callback declines delete_post delete_post: Operation rejected by approval handler: delete_post ``` **An agent can't approve its own tool.** When an agent writes a new tool at runtime, someone else has to approve it. `matimo_approve_tool` refuses a tool the same agent created, and no setting such as `MATIMO_AUTO_APPROVE` can skip that prompt. Roles like `admin` are set by your application, never read from what the agent writes. One caveat worth knowing: the self-approval check needs your app to pass the agent's `agentId`. Without a context, the human approval prompt is the only gate, so route it to a real person. **An audit log you can verify.** Every call ends in a `tool:executed` or `tool:execution_failed` event. Add a `JsonlFileSink` and each event is written to a hash-chained file with secrets redacted. From the Python demo (`audit_log_demo.py`): ``` {'valid': True, 'entries': 5, 'line': None, 'reason': None} After editing line 2: {'valid': False, 'entries': 1, 'line': 2, 'reason': 'hash does not match the entry'} ... {"user": "ada", "apiKey": "[REDACTED]", "headers": {"Authorization": "[REDACTED]"}} ``` An edited, removed or reordered line is found. This makes the log tamper-evident, not tamper-proof: someone who can rewrite the whole file can rebuild the chain, so keep the log where the agent host can't write. **The same rules in both SDKs.** Shared fixtures in `conformance/` pin approval, quarantine, events and the audit chain, and both SDKs are tested against them. A log written by one SDK verifies in the other. **Also in 0.2.0:** per-call and per-instance approval callbacks, MCP approval through elicitation instead of telling the model to retry, skill search and loading as meta-tools (15 in all), MCP tool annotations and structured errors, a response-size guardrail, and Agno support in Python. ## Upgrading This is a minor version with behaviour changes. If you have DELETE or command tools that used to run without a prompt, they now ask. Read [Upgrading to 0.2.0](https://docs.matimo.dev/api-reference/POLICY_AND_LIFECYCLE#upgrading-to-020) first. Setting `governanceMode: 'legacy'` restores the old default while you migrate, and `requires_approval: false` opts a single tool out. ```bash npm install @matimo/core # TypeScript pip install matimo # Python ``` ## Try it with no API key ```bash cd typescript/examples/tools pnpm policy:approval-modes # who gets asked, and what happens with no one to ask pnpm policy:audit # tamper detection on a hash-chained log ``` ## What Matimo OSS is, and isn't Matimo OSS is an MIT-licensed layer that runs inside your process. It governs tool calls; it doesn't replace your own authentication or hold your credentials. It has no compliance certifications. If you want managed integrations and OAuth at scale, Matimo can wrap a service such as Composio's catalog under the same policy layer, using your own key. Source, docs and the full 0.2.0 notes: [github.com/tallclub/matimo](https://github.com/tallclub/matimo) and [matimo.dev/docs](https://matimo.dev/docs/). Questions or a security report? [Discord](https://discord.gg/3JPt4mxWDV) or the repo's security page. --- # Troubleshooting & FAQ Source: https://matimo.dev/docs/troubleshooting/faq/ Common issues and solutions for Matimo 0.2.0 (TypeScript & Python). Upgrading from 0.1.x? Start with [Governance in 0.2.0](#governance-in-020). ## Installation & Setup ### TypeScript Issues #### Q: Node.js version error **Error**: `Node.js 22.19+ required. You are running v20.x.x` (from `matimo doctor`), or an `EBADENGINE` warning from npm/pnpm Since 0.3.0 every `@matimo/*` package needs Node 22.19 or newer. Node 18 and 20 are no longer supported. **Solution**: ```bash node --version # If below v22.19, install from https://nodejs.org # Use nvm (Node Version Manager) for easy switching: nvm install 22 nvm use 22 ``` #### Q: pnpm not found **Error**: `pnpm: command not found` **Solution**: ```bash npm install -g pnpm@8.15.0 pnpm --version ``` #### Q: Build fails with TypeScript errors **Error**: `Found X errors in src/**` **Solution**: ```bash # Clear and rebuild pnpm clean pnpm install pnpm build # If still failing, check TypeScript version pnpm list typescript # Should be 5.9.3 or higher ``` ### Python Issues #### Q: Python version too old **Error**: `ModuleNotFoundError: No module named 'matimo'` or `Python 3.10 or lower detected` **Solution**: ```bash python --version # If < 3.11, upgrade: # macOS: brew install python@3.11 # Linux: sudo apt install python3.11 # Windows: https://www.python.org/downloads/ # Verify: python3.11 --version # Create virtual environment python3.11 -m venv venv source venv/bin/activate # or 'venv\Scripts\activate' on Windows ``` #### Q: `pip install matimo` fails **Error**: `ERROR: Could not find a version that satisfies the requirement` **Solution**: ```bash # Make sure you're using Python 3.11+ python --version # Try with uv (faster): uv add matimo # Creates venv automatically # Or explicit pip: pip install --upgrade pip pip install matimo # For specific version: pip install matimo==0.2.0 ``` #### Q: LangChain or CrewAI imports fail **Error**: `ModuleNotFoundError: No module named 'langchain'` / `ModuleNotFoundError: No module named 'crewai'` **Solution**: ```bash # Install with extras: pip install "matimo-core[langchain]" #or pip install "matimo-core[crewai]" # With uv: uv add "matimo-core[langchain]" uv add "matimo-core[crewai]" # Verify: python -c "from matimo.integrations.langchain import convert_tools_to_langchain; print('LangChain integration available')" ``` #### Q: Async/await RuntimeError in Jupyter **Error**: `RuntimeError: asyncio.run() cannot be called from a running event loop` **Solution**: Jupyter already runs an event loop, so use top-level `await` instead of `asyncio.run()`: ```python from matimo import Matimo matimo = await Matimo.init() ``` If you have to call `asyncio.run()` (for example in code shared with a script), install `nest_asyncio` and apply it first: ```bash pip install nest_asyncio ``` ```python import nest_asyncio nest_asyncio.apply() ``` #### Q: CrewAI tools not executing **Error**: `Tool execution timeout` or `Tool not callable` **Solution**: ```python # Verify tools are created correctly tools = convert_tools_to_crewai(tool_definitions, matimo) print(f"Created {len(tools)} tools") print([f"{t.name}: {t.description}" for t in tools]) # Verify names # Ensure matimo.execute() is still available to the tool # It's captured in the closure, so it should work ``` --- ## Security & Embedded Code ### Q: Embedded code execution fails with "disabled" error **Error**: `Embedded code execution is disabled by default for security` **Explanation**: Matimo disables inline code execution in tool YAML to prevent Remote Code Execution (RCE) if tool definitions come from untrusted sources. **Default behavior**: ```typescript // embeddedCodeDisabled = (MATIMO_ALLOW_EMBEDDED_CODE !== 'true') // If env var is NOT set → embeddedCodeDisabled = true → throws error ✓ if (embeddedCodeDisabled) { throw new MatimoError('Embedded code execution is disabled by default...'); } ``` **Solutions**: 1. **Recommended**: Use external TypeScript files instead: ```yaml # Secure: External file execution: type: function code: ./handler.ts # Version-controlled, auditable timeout: 10000 ``` 2. **If you must use embedded code** (NOT recommended): ```bash # Only in trusted environments export MATIMO_ALLOW_EMBEDDED_CODE=true # Then run your tool pnpm agent:factory ``` ### Q: What's the security risk with embedded code? **Risk**: `new Function()` executes arbitrary JavaScript with access to: - `fs` module (file system read/write) - `path` module (path manipulation) - `axios` module (network requests) **If tool YAML comes from untrusted sources**, an attacker could inject malicious code that: - Steals environment variables (tokens, keys) - Modifies files - Makes unauthorized API requests **Mitigation**: **DO**: - Use external .ts files for tool logic - Review all tool YAML before loading - Only enable embedded code in fully-trusted environments - Never commit embedded code to repositories **DON'T**: - Load tool YAML from untrusted sources without review - Enable embedded code in production - Allow users to upload arbitrary tool definitions - Store secrets in tool YAML --- ## Tool Execution ### Q: Tool not found error **Error**: `Tool not found: calculator` **Solutions**: 1. Verify tool exists: ```bash pnpm validate-tools # Lists all loaded tools ``` 2. Check tools directory: ```bash # Ensure tools/calculator/definition.yaml exists ls -la tools/calculator/definition.yaml ``` 3. Verify tool definition: ```bash pnpm validate-tools # Shows validation errors if YAML is invalid ``` ### Q: Invalid parameters error **Error**: `Tool validation failed: Missing required parameter: operation` **Solutions**: 1. Check tool parameters: ```typescript const tool = matimoInstance.getTool('calculator'); console.log(tool.parameters); // See required fields ``` 2. Verify your input matches schema: ```typescript // Wrong await m.execute('calculator', { value: 5 }); // Correct await m.execute('calculator', { operation: 'add', a: 5, b: 3, }); ``` 3. Check tool spec: ```bash cat tools/calculator/definition.yaml | grep -A 20 "parameters:" ``` ### Q: HTTP tool returns 401 Unauthorized **Error**: `HTTP 401: Unauthorized` **Common causes**: 1. Missing OAuth token: ```bash # Set required tokens export GMAIL_ACCESS_TOKEN=your_token export GITHUB_TOKEN=your_token ``` 2. Expired token: ```typescript // Token needs refresh const newToken = await oauth2Handler.refreshTokenIfNeeded(userId, currentToken); ``` 3. Wrong token format: ```typescript // Verify token is Bearer token, not full "Bearer token" const token = process.env.GMAIL_ACCESS_TOKEN; // Should be: "ya29.abc123..." not "Bearer ya29.abc123..." ``` ### Q: Command executor fails **Error**: `Command not found: node` or shell command fails **Solutions**: 1. Verify command exists: ```bash which node which python ``` 2. Check working directory: ```yaml execution: type: command command: ./script.sh cwd: /path/to/directory # Ensure this exists ``` 3. Test command manually: ```bash # Test the exact command node calculator.js --op add 5 3 ``` --- ## OAuth2 & Authentication ### Q: OAuth token missing error **Error**: `Missing OAuth token for provider: google` **Solutions**: 1. Set environment variables: ```bash export GMAIL_ACCESS_TOKEN=ya29.your_token_here export GITHUB_TOKEN=ghp_your_token_here export SLACK_TOKEN=xoxb-your_token_here ``` 2. Verify token is set: ```typescript console.log(process.env.GMAIL_ACCESS_TOKEN); // Should print your token, not undefined ``` 3. Check tool authentication config: ```yaml authentication: type: oauth2 provider: google # Must match provider definition ``` ### Q: OAuth provider configuration not found **Error**: `Provider definition not found: github` **Solutions**: 1. Verify provider YAML exists: ```bash ls tools/github/definition.yaml # Should exist and contain: type: provider ``` 2. Check provider name: ```bash pnpm validate-tools # Lists all loaded providers ``` 3. Verify provider YAML format: ```bash cat tools/github/definition.yaml | head -5 # Should show: type: provider ``` ### Q: Bearer token format issue **Error**: `Invalid authorization header` **Solutions**: 1. Use correct token format: ```bash # Correct: Just the token export GMAIL_ACCESS_TOKEN=ya29.abc123 # Wrong: Don't include "Bearer" export GMAIL_ACCESS_TOKEN="Bearer ya29.abc123" ``` 2. Verify token is actually a token: ```bash echo $GMAIL_ACCESS_TOKEN | wc -c # Should be > 20 characters ``` --- ## Validation Errors ### Q: Schema validation failed **Error**: `Tool schema validation failed: • execution: Required` **Solutions**: 1. Check required fields in tool YAML: ```yaml # All required fields name: my-tool version: 1.0.0 description: Tool description parameters: {} execution: type: http method: GET url: https://api.example.com ``` 2. Run validation: ```bash pnpm validate-tools # Shows exact field errors ``` ### Q: Invalid execution type **Error**: `Invalid execution type: webhook` **Solutions**: Supported execution types are: - `command` - Shell commands - `http` - HTTP requests Unsupported (coming in future releases): - `webhook` - v0.3.0 - `grpc` - Future - `database` - Future --- ## Testing & Development ### Q: Tests fail locally **Error**: Tests pass in CI but fail locally **Solutions**: 1. Clear cache and reinstall: ```bash pnpm clean rm -rf node_modules pnpm-lock.yaml pnpm install ``` 2. Run specific test: ```bash pnpm test -- --testNamePattern="oauth2" ``` 3. Debug test: ```bash node --inspect-brk ./node_modules/.bin/jest --runInBand # Then open chrome://inspect ``` ### Q: Linting errors **Error**: `ESLint errors found` **Solution**: ```bash # Fix automatically pnpm lint:fix # Then format pnpm format ``` --- ## Performance Issues ### Q: Tool execution is slow **Possible causes**: 1. Network latency (HTTP tools) 2. OAuth token refresh overhead 3. Tool parameter validation **Solutions**: ```typescript // Time tool execution console.time('tool-execution'); const result = await m.execute('gmail-send-email', params); console.timeEnd('tool-execution'); // Optimize: Cache tools const tool = m.getTool('gmail-send-email'); const result = await executor.execute(tool, params); ``` ### Q: High memory usage **Solutions**: ```typescript // Don't load all tools if not needed const tools = m.listTools(); const filtered = tools.filter((t) => t.tags.includes('gmail')); // Or use specific tool const tool = m.getTool('gmail-send-email'); ``` --- ## Governance in 0.2.0 0.2.0 is secure by default. These are the errors you are most likely to meet after upgrading, and what to do. The full list of changes is in the [migration guide](https://matimo.dev/docs/api-reference/policy-and-lifecycle.md#upgrading-to-020). ### Q: `Destructive operation requires approval: ` The call needs a person's approval and nothing can answer. In 0.2.0 that covers tools with `requires_approval: true`, HTTP `DELETE` and `type: command` tools that don't set `requires_approval`, and calls whose `sql`/`command` argument contains a destructive keyword. - Give the instance a reviewer: `MatimoInstance.init({ onApproval })` / `Matimo.init(on_approval=...)`. - Or pre-approve tools that are safe in your setting: `MATIMO_APPROVED_PATTERNS="get_*,list_*"`. - Or opt one tool out with `requires_approval: false` in its YAML, or restore the pre-0.2.0 default with `governanceMode: 'legacy'` while you migrate. See [Approval System](https://matimo.dev/docs/api-reference/approval-system.md). ### Q: `Operation rejected by approval handler: ` Your `onApproval` callback returned `false` (or threw). That is the intended outcome of a decline. ### Q: `Draft tool "" requires admin role` / `is not available in production` The tool's YAML has `status: draft`, every tool an agent creates starts that way. A draft never runs in production and runs elsewhere only for a caller whose `PolicyContext` has the `admin` role. Have a reviewer approve it with `matimo_approve_tool` (or the `matimo review approve` CLI), then reload the tools. ### Q: An approved tool disappears on the next reload Check `rejected` in the reload result and the `tool:rejected` event. Common causes: the file changed after approval (the approval no longer matches), the tool breaks one of the developer's policy rules (`blocked-domain`, `blocked-http-method`, `unauthorized-credential`), or the approval was made by another process without the same `MATIMO_APPROVAL_SECRET` and `approvalDir`. ### Q: `Tool '' needs human approval, but this MCP client does not support elicitation` Over MCP, Matimo asks the person behind the client through an elicitation request. Use a client that supports it, pre-approve the tool on the server with `MATIMO_APPROVED_PATTERNS`, or, only if the client confirms every call with its user, start the server with `trustClientApproval: true`. See [MCP](https://matimo.dev/docs/mcp-server.md#approval-over-mcp). ### Q: A read-only SQL query asks for approval The keyword scan matches substrings of the upper-cased query, so a column like `created_at` contains `CREATE`. Approve it, or give read-only queries their own tool. ### Q: Should I set `MATIMO_AUTO_APPROVE=true` in CI? Prefer an `onApproval` that encodes what the test allows, or `MATIMO_APPROVED_PATTERNS`. `MATIMO_AUTO_APPROVE` approves everything unseen (except `matimo_approve_tool`) and logs a warning. --- ## LangChain Integration Issues ### Q: LangChain tool schema validation fails **Error**: `Invalid input` or schema validation error when invoking LangChain tool **Solutions**: 1. Check enum parameter handling: ```typescript // If tool has enum constraints, they're now properly validated const tool = m.getTool('my-tool'); console.log(tool.parameters.status.enum); // Check allowed values // Pass only allowed values await langTool.invoke({ status: 'active' }); // ``` 2. Verify default values are set: ```typescript // Tools with default values should apply them automatically await langTool.invoke({}); // Defaults injected ``` 3. Check secret parameter detection: ```typescript // Parameters with "token", "key", "secret", "password" are auto-hidden // Only real secret patterns are filtered (not "monkey", "turkey_id", etc.) const schema = langTool.schema; console.log(Object.keys(schema.shape)); // Won't include api_key, but will include data ``` --- ## Parameter Validation Issues ### Q: Enum parameter rejected valid value **Error**: `Invalid option: expected one of "active"|"inactive"` **Solutions**: 1. Verify parameter has enum defined: ```yaml parameters: status: type: string enum: ['active', 'inactive', 'pending'] ``` 2. Pass exact value: ```typescript // Correct await m.execute('tool', { status: 'active' }); // Wrong await m.execute('tool', { status: 'ACTIVE' }); // Case sensitive ``` ### Q: Default value not applied **Error**: Tool requires parameter but default wasn't used **Solutions**: 1. Mark parameter as optional if default provided: ```yaml parameters: timeout: type: number required: false # Important! default: 5000 ``` 2. Verify default is valid type: ```yaml # Correct default: 5000 # Wrong (must match type) default: "5000" # for number type ``` --- ## File Path & URL Issues ### Q: Function executor fails with "Cannot find module" **Error**: `Error: Cannot find module 'file:///path/to/tool.ts'` **Solutions**: 1. Uses proper file URLs (Windows compatibility fixed): ```typescript // Now uses pathToFileURL() for robust URL construction // Handles spaces, special chars, and Windows paths automatically ``` 2. Verify file exists: ```bash ls -la tools/provider/tool-name/tool-name.ts # Should exist and be readable ``` 3. Use relative paths in tool definition: ```yaml execution: type: function code: ./tool-name.ts # Relative to tool directory ``` --- ## Secret Detection Issues ### Q: Parameter incorrectly treated as secret **Error**: Parameter missing from LangChain schema (e.g., "monkey", "turkey_id") **Root Cause**: Old substring matching was too loose **Solution**: Now uses word-boundary matching: ```typescript // Old: Contains "key" substring → treated as secret // "monkey", "turkey_id", "donkey" → incorrectly hidden // New: Word-boundary matching // "monkey" → NOT a secret ("key" is substring) // "api_key" → IS a secret ("_key" is word boundary) // "getSecret" → IS a secret ("Secret" is word boundary) ``` **Verify parameter detection**: ```typescript const langTools = await convertToolsToLangChain([tool], matimo, { api_key: 'secret' }); const schema = langTools[0].schema; console.log(Object.keys(schema.shape)); // Should include: monkey, turkey_id, data // Should exclude: api_key, real_secret ``` --- Still stuck? Try: 1. **GitHub Issues**: [Search issues](https://github.com/tallclub/matimo/issues) 2. **GitHub Discussions**: [Ask questions](https://github.com/tallclub/matimo/discussions) 3. **Documentation**: [Full docs](/docs/) 4. **Examples**: [See examples](https://github.com/tallclub/matimo/blob/main/typescript/examples/) --- ## Report a Bug Found a bug? Help us fix it: ```bash # 1. Check if issue exists # Go to https://github.com/tallclub/matimo/issues # 2. If not, create new issue with: # - Matimo version: (run: npm list matimo) # - Node.js version: (run: node --version) # - Steps to reproduce # - Expected vs actual behavior # - Error message/stack trace ``` Thank you for helping improve Matimo!