Skip to content

Matimo MCP Server

Expose your Matimo tools to AI assistants via the Model Context Protocol. One command gives Claude Desktop, Cursor, Windsurf, or any MCP client access to Slack, GitHub, Gmail, Notion, HubSpot, and more.



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%+
  • 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
  • Python: python/packages/core/src/matimo/mcp/, see README.md

Terminal window
mkdir my-ai-tools && cd my-ai-tools
npm init -y

2. Install Matimo + the tool packages you want

Section titled “2. Install Matimo + the tool packages you want”
Terminal window
npm install @matimo/core @matimo/cli @matimo/slack @matimo/github
Terminal window
export SLACK_BOT_TOKEN=xoxb-your-slack-token
export GITHUB_TOKEN=ghp_your-github-token
Terminal window
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.

Terminal window
npx matimo mcp

That’s it. All installed @matimo/* tools are auto-discovered and exposed over stdio.

  1. Create a new project (or use an existing one)
Terminal window
mkdir my-ai-tools && cd my-ai-tools
python -m venv .venv
source .venv/bin/activate # or .venv\Scripts\activate on Windows
  1. Install Matimo + the tool packages you want
Terminal window
pip install matimo matimo[mcp]
# Add specific tool packages (optional)
pip install matimo-slack matimo-github matimo-gmail matimo-notion
  1. Set your API keys
Terminal window
export SLACK_BOT_TOKEN=xoxb-your-slack-token
export GITHUB_TOKEN=ghp_your-github-token
  1. Start the server
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:

Terminal window
python mcp_server.py

That’s it. All installed matimo-* tools are auto-discovered and exposed over stdio.


  • Node.js 22.19+: Download
  • npm or pnpm: comes with Node.js

Pick the tool packages you need:

Terminal window
# 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:

Terminal window
npm install @matimo/core @matimo/cli @matimo/slack @matimo/github @matimo/gmail @matimo/notion @matimo/hubspot @matimo/postgres @matimo/twilio @matimo/mailchimp

Each tool package requires its own API credentials. Set them as environment variables:

Terminal window
# 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.

Terminal window
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": "<your-token>"
}
}
}
}

  1. Open Claude Desktop
  2. Go to Settings → Developer → Edit Config
  3. Paste the config from npx matimo mcp setup, or use this template:
{
"mcpServers": {
"matimo": {
"command": "npx",
"args": ["matimo", "mcp"],
"env": {
"SLACK_BOT_TOKEN": "xoxb-your-token",
"GITHUB_TOKEN": "ghp_your-token"
}
}
}
}
  1. Restart Claude Desktop
  2. 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.

Create .cursor/mcp.json in your project root:

{
"mcpServers": {
"matimo": {
"command": "npx",
"args": ["matimo", "mcp"],
"env": {
"SLACK_BOT_TOKEN": "xoxb-your-token",
"GITHUB_TOKEN": "ghp_your-token"
}
}
}
}

Create .windsurf/mcp.json in your project root:

{
"mcpServers": {
"matimo": {
"command": "npx",
"args": ["matimo", "mcp"],
"env": {
"SLACK_BOT_TOKEN": "xoxb-your-token",
"GITHUB_TOKEN": "ghp_your-token"
}
}
}
}
  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:
{
"mcpServers": {
"matimo": {
"command": "python",
"args": ["/path/to/mcp_server.py"],
"env": {
"SLACK_BOT_TOKEN": "xoxb-your-token",
"GITHUB_TOKEN": "ghp_your-token"
}
}
}
}
  1. Restart Claude Desktop
  2. Look for the tools icon, your Matimo tools should appear

Create .cursor/mcp.json in your project root:

{
"mcpServers": {
"matimo": {
"command": "python",
"args": ["mcp_server.py"],
"env": {
"SLACK_BOT_TOKEN": "xoxb-your-token",
"GITHUB_TOKEN": "ghp_your-token"
}
}
}
}

Create .windsurf/mcp.json in your project root:

{
"mcpServers": {
"matimo": {
"command": "python",
"args": ["mcp_server.py"],
"env": {
"SLACK_BOT_TOKEN": "xoxb-your-token",
"GITHUB_TOKEN": "ghp_your-token"
}
}
}
}

For remote servers, Docker containers, or MCP clients that connect over HTTP/HTTPS:

Terminal window
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 <value>

Configure your MCP client with the displayed URL and token. For clients that support remote MCP:

{
"mcpServers": {
"matimo": {
"url": "http://your-server:3000/mcp",
"headers": {
"Authorization": "Bearer 550e8400-e29b-41d4-a716-446655440000"
}
}
}
}

Start the server:

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:

Terminal window
python mcp_server_http.py

Configure your MCP client:

{
"mcpServers": {
"matimo": {
"url": "http://localhost:3000/mcp",
"headers": {
"Authorization": "Bearer your-secret-token"
}
}
}
}

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

Both TypeScript and Python SDKs discover tools identically via entry points + filesystem scan:

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
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.

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();
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())

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.


matimo mcp [options] Start the MCP server
matimo mcp setup Run the setup wizard
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
Terminal window
# 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 runs a stateful HTTP server for remote access, Docker containers, or MCP clients that support HTTP transport.

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":<count>}
OPTIONS * No CORS preflight

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 <token> in every request (except /health).

Terminal window
# Test health (no auth needed)
curl http://localhost:3000/health
# Test auth: initialize a new session
curl -H "Authorization: Bearer <token>" \
-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 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.

Matimo HTTP mode is built on the official @modelcontextprotocol/sdk and implements the Streamable HTTP transport 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: 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: invalid version must return 400 No impact with current MCP SDK clients; all known clients send a valid version

Enable HTTPS for encrypted connections. Two options:

Terminal window
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) 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
Terminal window
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.

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"]
Terminal window
# 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:

Terminal window
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

The MCP server needs API keys and tokens to call tool APIs. Matimo supports four secret resolver backends that can be chained.

  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

Reads from environment variables. Checks MATIMO_<KEY> first, then <KEY>.

Terminal window
export SLACK_BOT_TOKEN=xoxb-your-token
npx matimo mcp

Reads from a .env file. Great for development.

.env
SLACK_BOT_TOKEN=xoxb-your-token
GITHUB_TOKEN=ghp_your-token
Terminal window
npx matimo mcp --secrets dotenv
npx matimo mcp --secrets dotenv --env-file ./config/.env

Reads from HashiCorp Vault KV v2. Requires node-vault peer dependency.

Terminal window
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.

Reads from AWS Secrets Manager. Requires @aws-sdk/client-secrets-manager peer dependency.

matimo/credentials
npm install @aws-sdk/client-secrets-manager
# Store secrets as JSON in AWS Secrets Manager:
# 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).

Resolvers are tried in order. First match wins.

Terminal window
# 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

Terminal window
npx matimo mcp --tools slack_send_channel_message,slack-list-channels

Denylist: expose everything except certain tools

Section titled “Denylist: expose everything except certain tools”
Terminal window
npx matimo mcp --exclude postgres-execute-sql

Both: allowlist first, then denylist filters the result

Section titled “Both: allowlist first, then denylist filters the result”
Terminal window
npx matimo mcp --tools slack_send_channel_message,slack_send_dm --exclude slack_send_dm

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), 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.

trustClientApproval and context are programmatic options (below); the matimo mcp CLI does not set them.


Every tool Matimo registers over MCP carries the protocol’s standard tool 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.

Failed tool calls return isError: true with a structuredContent field carrying machine-readable error data, instead of only a freeform error string:

{
"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 for the full code list and what retryable means for each.


Use the MCP server directly from 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();
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://<name> 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)
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.


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. 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.

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. 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.

The Python MCP implementation follows the TypeScript one; both SDKs serve MCP (see known differences in the release notes):

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
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

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<string, string>
  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.


  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:
    Terminal window
    npx matimo mcp setup

No tools found / “No @matimo/* tool packages found”

Section titled “No tools found / “No @matimo/* tool packages found””

Make sure @matimo/* packages are installed in your project:

Terminal window
npm ls | grep matimo
# Should show @matimo/core, @matimo/cli, @matimo/slack, etc.

If empty, install them:

Terminal window
npm install @matimo/core @matimo/cli @matimo/slack

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.

Requires openssl to be installed:

Terminal window
# 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)

Section titled “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.


Section titled “Option 1: Use a locally trusted certificate (recommended)”

mkcert creates certificates signed by a local CA that your OS and browser trust automatically. No bypass needed, no security warnings.

Terminal window
# 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.


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:

Terminal window
npx matimo mcp --transport http --cert cert.pem --key key.pem

Option 3: Narrow TLS bypass for development clients only

Section titled “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:

Terminal window
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.

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.

Enable verbose logging to see tool discovery, auth resolution, and execution details:

Terminal window
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.

Vault and AWS secret resolvers require extra packages:

Terminal window
npm install node-vault # For --secrets vault
npm install @aws-sdk/client-secrets-manager # For --secrets aws

Error: MatimoError(EXECUTION_FAILED): MCP Python SDK not installed. Install with: pip install matimo[mcp]

Solution:

Terminal window
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".

Error: Tools list is empty in Claude Desktop

Solution:

  1. Verify Matimo can find your tools:
from matimo import Matimo
matimo = await Matimo.init(auto_discover=True)
print(f"Found {len(matimo.list_tools())} tools")
  1. If empty, ensure tool packages are installed:
Terminal window
pip list | grep matimo
# Should show matimo, matimo-slack, matimo-github, etc.
  1. If missing, install them:
Terminal window
pip install matimo matimo-slack matimo-github

“Stdio mode corrupt output / JSON-RPC decode error”

Section titled ““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:

# 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)

Error: 401 Unauthorized when connecting to HTTP mode

Solution:

  1. Check the bearer token is set:
Terminal window
export MATIMO_MCP_TOKEN="your-secret-token"
python mcp_server.py
  1. Pass it in the MCP client config:
{
"mcpServers": {
"matimo": {
"url": "http://localhost:3000/mcp",
"headers": {
"Authorization": "Bearer your-secret-token"
}
}
}
}

Error: KeyError: 'SLACK_BOT_TOKEN' during tool execution

Solution:

Ensure your .env file is readable and in the project root:

.env
SLACK_BOT_TOKEN=xoxb-your-token
GITHUB_TOKEN=ghp_your-token

Also verify Matimo is initialized with proper paths:

from matimo import Matimo
matimo = await Matimo.init(
["./tools"], # ← tool paths
auto_discover=True
)

Error: RuntimeError: no running event loop or RuntimeError: asyncio.run() called from a running event loop

Solution:

Use asyncio.run() at the top level:

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.