matimo-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matimo_cli-0.1.0/.gitignore +63 -0
- matimo_cli-0.1.0/PKG-INFO +132 -0
- matimo_cli-0.1.0/README.md +116 -0
- matimo_cli-0.1.0/pyproject.toml +29 -0
- matimo_cli-0.1.0/src/matimo_cli/__init__.py +1 -0
- matimo_cli-0.1.0/src/matimo_cli/__main__.py +4 -0
- matimo_cli-0.1.0/src/matimo_cli/cli.py +97 -0
- matimo_cli-0.1.0/src/matimo_cli/commands/__init__.py +1 -0
- matimo_cli-0.1.0/src/matimo_cli/commands/doctor.py +140 -0
- matimo_cli-0.1.0/src/matimo_cli/commands/install.py +37 -0
- matimo_cli-0.1.0/src/matimo_cli/commands/list_cmd.py +44 -0
- matimo_cli-0.1.0/src/matimo_cli/commands/mcp.py +216 -0
- matimo_cli-0.1.0/src/matimo_cli/commands/mcp_setup.py +104 -0
- matimo_cli-0.1.0/src/matimo_cli/commands/review.py +171 -0
- matimo_cli-0.1.0/src/matimo_cli/commands/search.py +139 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
**/node_modules/
|
|
3
|
+
package-lock.json
|
|
4
|
+
yarn.lock
|
|
5
|
+
|
|
6
|
+
# Build output
|
|
7
|
+
**/dist/
|
|
8
|
+
*.tsbuildinfo
|
|
9
|
+
|
|
10
|
+
# Python compiled / build
|
|
11
|
+
**/__pycache__/
|
|
12
|
+
*.py[cod]
|
|
13
|
+
*$py.class
|
|
14
|
+
*.egg-info/
|
|
15
|
+
*.egg
|
|
16
|
+
**/build/
|
|
17
|
+
**/.eggs/
|
|
18
|
+
**/.venv/
|
|
19
|
+
**/.mypy_cache/
|
|
20
|
+
**/.ruff_cache/
|
|
21
|
+
**/.pytest_cache/
|
|
22
|
+
|
|
23
|
+
# Test coverage
|
|
24
|
+
**/coverage/
|
|
25
|
+
**/.nyc_output/
|
|
26
|
+
|
|
27
|
+
# IDE
|
|
28
|
+
.vscode/
|
|
29
|
+
.idea/
|
|
30
|
+
*.swp
|
|
31
|
+
*.swo
|
|
32
|
+
*~
|
|
33
|
+
.DS_Store
|
|
34
|
+
|
|
35
|
+
# Environment
|
|
36
|
+
.env
|
|
37
|
+
.env.local
|
|
38
|
+
.env.*.local
|
|
39
|
+
|
|
40
|
+
# Logs
|
|
41
|
+
*.log
|
|
42
|
+
npm-debug.log*
|
|
43
|
+
yarn-debug.log*
|
|
44
|
+
yarn-error.log*
|
|
45
|
+
|
|
46
|
+
# OS
|
|
47
|
+
.DS_Store
|
|
48
|
+
Thumbs.db
|
|
49
|
+
|
|
50
|
+
# Temporary files
|
|
51
|
+
tmp/
|
|
52
|
+
temp/
|
|
53
|
+
*.tmp
|
|
54
|
+
typescript/examples/mcp/matimo-tools/.matimo-approvals.json
|
|
55
|
+
typescript/examples/mcp/matimo-tools/fetch-weather/definition.yaml
|
|
56
|
+
typescript/examples/mcp/matimo-tools/npm_downloads/definition.yaml
|
|
57
|
+
typescript/examples/mcp/matimo-tools/skills/ecosystem-health/SKILL.md
|
|
58
|
+
typescript/examples/mcp/matimo-tools/skills/matimo-health-check/SKILL.md
|
|
59
|
+
typescript/examples/mcp/matimo-tools/skills/moltbook-identity/SKILL.md
|
|
60
|
+
typescript/packages/cli/.matimo/certs/server.crt
|
|
61
|
+
typescript/packages/cli/.matimo/certs/server.key
|
|
62
|
+
typescript/examples/mcp/.matimo/certs/server.crt
|
|
63
|
+
typescript/examples/mcp/.matimo/certs/server.key
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: matimo-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Matimo CLI — tool package manager & MCP server launcher
|
|
5
|
+
Author: Matimo
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: ai-tools,cli,matimo,mcp
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Requires-Dist: matimo-core[mcp]<0.2.0,>=0.1.0a14.post1
|
|
13
|
+
Requires-Dist: mcp>=1.0
|
|
14
|
+
Requires-Dist: pyyaml>=6.0
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# matimo-cli
|
|
18
|
+
|
|
19
|
+
> Command-line interface for [Matimo](https://matimo.dev) — tool package manager & MCP server launcher.
|
|
20
|
+
|
|
21
|
+
[](https://pypi.org/project/matimo-cli/)
|
|
22
|
+
[](https://matimo.dev/docs)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install matimo-cli
|
|
30
|
+
# or with uv
|
|
31
|
+
uv add matimo-cli
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Commands
|
|
37
|
+
|
|
38
|
+
### `matimo install` — Install provider packages
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
matimo install slack github gmail # install specific providers
|
|
42
|
+
matimo install slack --upgrade # upgrade an existing provider
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### `matimo list` — List available tools
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
matimo list # all loaded tools
|
|
49
|
+
matimo list --provider slack # filter by provider
|
|
50
|
+
matimo list --format json # JSON output
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `matimo search` — Search for tools
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
matimo search email # text search over tool names + descriptions
|
|
57
|
+
matimo search "send message"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### `matimo mcp` — Start MCP server
|
|
61
|
+
|
|
62
|
+
Serve all loaded tools over the [Model Context Protocol](https://matimo.dev/docs/MCP) so Claude Desktop, Cursor, or any MCP client can access them.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
matimo mcp # start on stdio (default)
|
|
66
|
+
matimo mcp --transport http --port 3000 # start as HTTP server
|
|
67
|
+
matimo mcp --name "my-agent" # set server name
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Claude Desktop `claude_desktop_config.json`:**
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"mcpServers": {
|
|
74
|
+
"matimo": {
|
|
75
|
+
"command": "matimo",
|
|
76
|
+
"args": ["mcp"]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `matimo doctor` — Diagnose your setup
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
matimo doctor # check config, installed providers, connectivity
|
|
86
|
+
matimo doctor --fix # attempt auto-repair
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### `matimo review` — Review agent-created tool definitions
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
matimo review ./agent-tools/ # interactive review of draft tools
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Configuration
|
|
98
|
+
|
|
99
|
+
The CLI reads Matimo configuration from the environment or a `.matimo.yaml` file in the project root:
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
# .matimo.yaml
|
|
103
|
+
toolPaths:
|
|
104
|
+
- ./tools
|
|
105
|
+
- ./agent-tools
|
|
106
|
+
logLevel: info
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Environment variables:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
export MATIMO_LOG_LEVEL=info # silent | error | warn | info | debug
|
|
113
|
+
export MATIMO_LOG_FORMAT=json # json | simple
|
|
114
|
+
export MATIMO_AUTO_APPROVE=true # auto-approve tool approval prompts (CI/CD)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Documentation
|
|
120
|
+
|
|
121
|
+
- [CLI Guide](https://matimo.dev/docs/user-guide/)
|
|
122
|
+
- [MCP Guide](https://matimo.dev/docs/MCP)
|
|
123
|
+
- [Getting Started](https://matimo.dev/docs/getting-started/QUICK_START)
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Links
|
|
128
|
+
|
|
129
|
+
- **PyPI:** https://pypi.org/project/matimo-cli/
|
|
130
|
+
- **Docs:** https://matimo.dev/docs
|
|
131
|
+
- **GitHub:** https://github.com/tallclub/matimo
|
|
132
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# matimo-cli
|
|
2
|
+
|
|
3
|
+
> Command-line interface for [Matimo](https://matimo.dev) — tool package manager & MCP server launcher.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/matimo-cli/)
|
|
6
|
+
[](https://matimo.dev/docs)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install matimo-cli
|
|
14
|
+
# or with uv
|
|
15
|
+
uv add matimo-cli
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Commands
|
|
21
|
+
|
|
22
|
+
### `matimo install` — Install provider packages
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
matimo install slack github gmail # install specific providers
|
|
26
|
+
matimo install slack --upgrade # upgrade an existing provider
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### `matimo list` — List available tools
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
matimo list # all loaded tools
|
|
33
|
+
matimo list --provider slack # filter by provider
|
|
34
|
+
matimo list --format json # JSON output
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### `matimo search` — Search for tools
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
matimo search email # text search over tool names + descriptions
|
|
41
|
+
matimo search "send message"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### `matimo mcp` — Start MCP server
|
|
45
|
+
|
|
46
|
+
Serve all loaded tools over the [Model Context Protocol](https://matimo.dev/docs/MCP) so Claude Desktop, Cursor, or any MCP client can access them.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
matimo mcp # start on stdio (default)
|
|
50
|
+
matimo mcp --transport http --port 3000 # start as HTTP server
|
|
51
|
+
matimo mcp --name "my-agent" # set server name
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Claude Desktop `claude_desktop_config.json`:**
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"matimo": {
|
|
59
|
+
"command": "matimo",
|
|
60
|
+
"args": ["mcp"]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### `matimo doctor` — Diagnose your setup
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
matimo doctor # check config, installed providers, connectivity
|
|
70
|
+
matimo doctor --fix # attempt auto-repair
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### `matimo review` — Review agent-created tool definitions
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
matimo review ./agent-tools/ # interactive review of draft tools
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Configuration
|
|
82
|
+
|
|
83
|
+
The CLI reads Matimo configuration from the environment or a `.matimo.yaml` file in the project root:
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
# .matimo.yaml
|
|
87
|
+
toolPaths:
|
|
88
|
+
- ./tools
|
|
89
|
+
- ./agent-tools
|
|
90
|
+
logLevel: info
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Environment variables:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
export MATIMO_LOG_LEVEL=info # silent | error | warn | info | debug
|
|
97
|
+
export MATIMO_LOG_FORMAT=json # json | simple
|
|
98
|
+
export MATIMO_AUTO_APPROVE=true # auto-approve tool approval prompts (CI/CD)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Documentation
|
|
104
|
+
|
|
105
|
+
- [CLI Guide](https://matimo.dev/docs/user-guide/)
|
|
106
|
+
- [MCP Guide](https://matimo.dev/docs/MCP)
|
|
107
|
+
- [Getting Started](https://matimo.dev/docs/getting-started/QUICK_START)
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Links
|
|
112
|
+
|
|
113
|
+
- **PyPI:** https://pypi.org/project/matimo-cli/
|
|
114
|
+
- **Docs:** https://matimo.dev/docs
|
|
115
|
+
- **GitHub:** https://github.com/tallclub/matimo
|
|
116
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "matimo-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Matimo CLI — tool package manager & MCP server launcher"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Matimo" }]
|
|
13
|
+
keywords = ["matimo", "cli", "ai-tools", "mcp"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"matimo-core[mcp]>=0.1.0a14.post1,<0.2.0",
|
|
21
|
+
"pyyaml>=6.0",
|
|
22
|
+
"mcp>=1.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
matimo = "matimo_cli.cli:main"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["src/matimo_cli"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Matimo CLI — Tool package manager & MCP server launcher."""
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Matimo CLI — main command router.
|
|
3
|
+
|
|
4
|
+
Mirrors: packages/cli/src/cli.ts
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
matimo install slack gmail
|
|
9
|
+
matimo list
|
|
10
|
+
matimo search slack
|
|
11
|
+
matimo mcp
|
|
12
|
+
matimo mcp setup
|
|
13
|
+
matimo doctor
|
|
14
|
+
matimo review list
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
from matimo_cli.commands.doctor import doctor_command
|
|
21
|
+
from matimo_cli.commands.install import install_command
|
|
22
|
+
from matimo_cli.commands.list_cmd import list_command
|
|
23
|
+
from matimo_cli.commands.mcp import mcp_command
|
|
24
|
+
from matimo_cli.commands.review import review_command
|
|
25
|
+
from matimo_cli.commands.search import search_command
|
|
26
|
+
|
|
27
|
+
_VERSION = "0.1.0" # also update in setup.py and pyproject.toml
|
|
28
|
+
|
|
29
|
+
_HELP = f"""\
|
|
30
|
+
🔨 Matimo CLI — Tool Package Manager (v{_VERSION})
|
|
31
|
+
|
|
32
|
+
Usage: matimo [command] [options]
|
|
33
|
+
|
|
34
|
+
Commands:
|
|
35
|
+
install <tools...> Install tool packages (pip install matimo-<name>)
|
|
36
|
+
list List installed Matimo tool packages
|
|
37
|
+
search <query> Search for available tools
|
|
38
|
+
mcp Start MCP server (Model Context Protocol)
|
|
39
|
+
mcp setup Generate config for Claude Desktop / Cursor
|
|
40
|
+
doctor Diagnose your Matimo setup
|
|
41
|
+
review Review agent-created tools awaiting approval
|
|
42
|
+
help Show this help message
|
|
43
|
+
version Show version information
|
|
44
|
+
|
|
45
|
+
Examples:
|
|
46
|
+
matimo install slack gmail
|
|
47
|
+
matimo list
|
|
48
|
+
matimo search email
|
|
49
|
+
matimo mcp
|
|
50
|
+
matimo mcp --transport http --port 3000
|
|
51
|
+
matimo mcp setup
|
|
52
|
+
matimo doctor
|
|
53
|
+
matimo review list
|
|
54
|
+
matimo review approve my_tool
|
|
55
|
+
|
|
56
|
+
Documentation: https://github.com/tallclub/matimo#readme
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main(cli_args: list[str] | None = None) -> None:
|
|
61
|
+
"""Main CLI handler — parses commands and routes to handlers."""
|
|
62
|
+
args = cli_args if cli_args is not None else sys.argv[1:]
|
|
63
|
+
command = args[0] if args else None
|
|
64
|
+
params = args[1:] if args else []
|
|
65
|
+
|
|
66
|
+
if not command:
|
|
67
|
+
print(_HELP)
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
match command.lower():
|
|
72
|
+
case "install":
|
|
73
|
+
install_command(params)
|
|
74
|
+
case "list":
|
|
75
|
+
list_command()
|
|
76
|
+
case "search":
|
|
77
|
+
search_command(params[0] if params else "")
|
|
78
|
+
case "mcp":
|
|
79
|
+
mcp_command(params)
|
|
80
|
+
case "doctor":
|
|
81
|
+
doctor_command()
|
|
82
|
+
case "review":
|
|
83
|
+
review_command(params)
|
|
84
|
+
case "help" | "-h" | "--help":
|
|
85
|
+
print(_HELP)
|
|
86
|
+
case "version" | "-v" | "--version":
|
|
87
|
+
print(f"matimo-cli v{_VERSION}")
|
|
88
|
+
case _:
|
|
89
|
+
print(f"❌ Unknown command: {command}", file=sys.stderr)
|
|
90
|
+
print('\nRun "matimo help" for available commands')
|
|
91
|
+
sys.exit(1)
|
|
92
|
+
except KeyboardInterrupt:
|
|
93
|
+
print("\nInterrupted.")
|
|
94
|
+
sys.exit(130)
|
|
95
|
+
except Exception as exc:
|
|
96
|
+
print(f"❌ Error: {exc}", file=sys.stderr)
|
|
97
|
+
sys.exit(1)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Matimo CLI commands."""
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
``matimo doctor`` — diagnose Matimo setup.
|
|
3
|
+
|
|
4
|
+
Mirrors: packages/cli/src/commands/doctor.ts
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
_AUTH_PATTERNS = {"TOKEN", "SECRET", "KEY", "PASSWORD", "CREDENTIAL", "AUTH"}
|
|
14
|
+
_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _is_auth_var(name: str) -> bool:
|
|
18
|
+
upper = name.upper()
|
|
19
|
+
return any(p in upper for p in _AUTH_PATTERNS)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def doctor_command() -> None:
|
|
23
|
+
issues: list[dict[str, str]] = []
|
|
24
|
+
|
|
25
|
+
def check(label: str, passed: bool, message: str, severity: str = "error") -> None:
|
|
26
|
+
icon = "✅" if passed else ("❌" if severity == "error" else "⚠️ ")
|
|
27
|
+
print(f" {icon} {label}")
|
|
28
|
+
if not passed:
|
|
29
|
+
issues.append({"severity": severity, "message": message})
|
|
30
|
+
print(f" {message}")
|
|
31
|
+
|
|
32
|
+
print("\n🩺 Matimo Doctor — Checking your setup…\n")
|
|
33
|
+
|
|
34
|
+
# 1. Python version
|
|
35
|
+
print("Python:")
|
|
36
|
+
v = sys.version_info
|
|
37
|
+
check(
|
|
38
|
+
f"Python {v.major}.{v.minor}.{v.micro}",
|
|
39
|
+
v >= (3, 11),
|
|
40
|
+
f"Python 3.11+ required. You are running {v.major}.{v.minor}. Upgrade: https://python.org",
|
|
41
|
+
)
|
|
42
|
+
print()
|
|
43
|
+
|
|
44
|
+
# 2. matimo package
|
|
45
|
+
print("matimo SDK:")
|
|
46
|
+
try:
|
|
47
|
+
import importlib.metadata
|
|
48
|
+
|
|
49
|
+
matimo_version = importlib.metadata.version("matimo")
|
|
50
|
+
check(f"matimo v{matimo_version}", True, "")
|
|
51
|
+
except importlib.metadata.PackageNotFoundError:
|
|
52
|
+
check("matimo", False, 'matimo package not installed. Run "pip install matimo".')
|
|
53
|
+
print()
|
|
54
|
+
|
|
55
|
+
# 3. Installed provider packages
|
|
56
|
+
print("matimo-* packages:")
|
|
57
|
+
try:
|
|
58
|
+
import importlib.metadata as md
|
|
59
|
+
|
|
60
|
+
providers = [
|
|
61
|
+
d
|
|
62
|
+
for d in md.distributions()
|
|
63
|
+
if d.metadata["Name"]
|
|
64
|
+
and d.metadata["Name"].startswith("matimo-")
|
|
65
|
+
and d.metadata["Name"] not in ("matimo-cli",)
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
if not providers:
|
|
69
|
+
check(
|
|
70
|
+
"matimo-* providers",
|
|
71
|
+
False,
|
|
72
|
+
'No matimo-* packages installed. Run "matimo install slack" to get started.',
|
|
73
|
+
"warn",
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
for dist in sorted(providers, key=lambda d: d.metadata["Name"]):
|
|
77
|
+
pkg_name = dist.metadata["Name"]
|
|
78
|
+
print(f" 📦 {pkg_name}")
|
|
79
|
+
|
|
80
|
+
# Try to find tools directory and scan for auth placeholders
|
|
81
|
+
for dist_file in dist.files or []:
|
|
82
|
+
str_path = str(dist_file)
|
|
83
|
+
if "tools/" in str_path and str_path.endswith("definition.yaml"):
|
|
84
|
+
full_path = Path(str(dist.locate_file(dist_file)))
|
|
85
|
+
if full_path.is_file():
|
|
86
|
+
content = full_path.read_text(encoding="utf-8")
|
|
87
|
+
missing = []
|
|
88
|
+
for m in _PLACEHOLDER_RE.finditer(content):
|
|
89
|
+
name = m.group(1)
|
|
90
|
+
if _is_auth_var(name) and not os.environ.get(name):
|
|
91
|
+
missing.append(name)
|
|
92
|
+
if missing:
|
|
93
|
+
for v in missing:
|
|
94
|
+
print(f" ❌ Missing env var: {v}")
|
|
95
|
+
issues.append(
|
|
96
|
+
{
|
|
97
|
+
"severity": "error",
|
|
98
|
+
"message": f"{pkg_name}: missing env vars: {', '.join(missing)}",
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
print(" ✅ All required env vars are set")
|
|
103
|
+
print()
|
|
104
|
+
except Exception:
|
|
105
|
+
check("matimo-* scan", False, "Failed to scan installed packages.", "warn")
|
|
106
|
+
print()
|
|
107
|
+
|
|
108
|
+
# 4. MATIMO_APPROVAL_SECRET
|
|
109
|
+
print("Policy / Approval:")
|
|
110
|
+
has_secret = bool(os.environ.get("MATIMO_APPROVAL_SECRET"))
|
|
111
|
+
check(
|
|
112
|
+
"MATIMO_APPROVAL_SECRET",
|
|
113
|
+
has_secret,
|
|
114
|
+
"MATIMO_APPROVAL_SECRET is not set. Agent-created tool approvals will use a "
|
|
115
|
+
"random secret (not persistent across restarts). Set it in your .env file.",
|
|
116
|
+
"warn",
|
|
117
|
+
)
|
|
118
|
+
print()
|
|
119
|
+
|
|
120
|
+
# 5. Summary
|
|
121
|
+
errors = [i for i in issues if i["severity"] == "error"]
|
|
122
|
+
warnings = [i for i in issues if i["severity"] == "warn"]
|
|
123
|
+
|
|
124
|
+
print("─" * 60)
|
|
125
|
+
if not errors and not warnings:
|
|
126
|
+
print("\n✅ Matimo is ready! No issues found.\n")
|
|
127
|
+
else:
|
|
128
|
+
if errors:
|
|
129
|
+
print(f"\n❌ {len(errors)} error(s) found — fix before using Matimo:\n")
|
|
130
|
+
for idx, e in enumerate(errors, 1):
|
|
131
|
+
print(f" {idx}. {e['message']}")
|
|
132
|
+
print()
|
|
133
|
+
if warnings:
|
|
134
|
+
print(f"⚠️ {len(warnings)} warning(s):\n")
|
|
135
|
+
for idx, w in enumerate(warnings, 1):
|
|
136
|
+
print(f" {idx}. {w['message']}")
|
|
137
|
+
print()
|
|
138
|
+
|
|
139
|
+
if errors:
|
|
140
|
+
sys.exit(1)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
``matimo install`` — install Matimo tool packages via pip.
|
|
3
|
+
|
|
4
|
+
Mirrors: packages/cli/src/commands/install.ts
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def install_command(tool_names: list[str]) -> None:
|
|
13
|
+
if not tool_names:
|
|
14
|
+
print("❌ Error: Please specify at least one tool to install", file=sys.stderr)
|
|
15
|
+
print("\nUsage: matimo install [tool1] [tool2] …")
|
|
16
|
+
print("Example: matimo install slack gmail stripe")
|
|
17
|
+
sys.exit(1)
|
|
18
|
+
|
|
19
|
+
packages = [f"matimo-{name}" for name in tool_names]
|
|
20
|
+
|
|
21
|
+
print(f"📦 Installing {', '.join(packages)}…")
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
subprocess.check_call(
|
|
25
|
+
[sys.executable, "-m", "pip", "install", *packages],
|
|
26
|
+
stdout=sys.stdout,
|
|
27
|
+
stderr=sys.stderr,
|
|
28
|
+
)
|
|
29
|
+
except subprocess.CalledProcessError:
|
|
30
|
+
print("❌ Installation failed.", file=sys.stderr)
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
|
|
33
|
+
print("\n✅ Installation complete!")
|
|
34
|
+
print("\nNext steps:")
|
|
35
|
+
print(" from matimo import Matimo")
|
|
36
|
+
print(" matimo = await Matimo.init(auto_discover=True)")
|
|
37
|
+
print("\n📖 For more info: https://github.com/tallclub/matimo#readme")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
``matimo list`` — list installed Matimo tool packages.
|
|
3
|
+
|
|
4
|
+
Mirrors: packages/cli/src/commands/list.ts
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import importlib.metadata
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def list_command() -> None:
|
|
13
|
+
"""List all installed matimo-* packages."""
|
|
14
|
+
try:
|
|
15
|
+
packages = [
|
|
16
|
+
dist
|
|
17
|
+
for dist in importlib.metadata.distributions()
|
|
18
|
+
if dist.metadata["Name"]
|
|
19
|
+
and dist.metadata["Name"].startswith("matimo-")
|
|
20
|
+
and dist.metadata["Name"] != "matimo-cli"
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
if not packages:
|
|
24
|
+
print("⚠️ No Matimo tool packages installed yet")
|
|
25
|
+
print("\nInstall some tools:")
|
|
26
|
+
print(" matimo install slack gmail")
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
print("📦 Installed Matimo Packages:\n")
|
|
30
|
+
|
|
31
|
+
for dist in sorted(packages, key=lambda d: d.metadata["Name"]):
|
|
32
|
+
name = dist.metadata["Name"]
|
|
33
|
+
version = dist.metadata["Version"]
|
|
34
|
+
summary = dist.metadata.get("Summary", "")
|
|
35
|
+
print(f" 📍 {name} (v{version})")
|
|
36
|
+
if summary:
|
|
37
|
+
print(f" {summary}")
|
|
38
|
+
print()
|
|
39
|
+
|
|
40
|
+
print(f"Total: {len(packages)} package{'s' if len(packages) != 1 else ''} installed")
|
|
41
|
+
|
|
42
|
+
except Exception as exc:
|
|
43
|
+
print(f"❌ Error listing tools: {exc}", file=sys.stderr)
|
|
44
|
+
sys.exit(1)
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""
|
|
2
|
+
``matimo mcp`` — start the Matimo MCP server.
|
|
3
|
+
|
|
4
|
+
Mirrors: packages/cli/src/commands/mcp.ts
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import signal
|
|
9
|
+
import sys
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class McpArgs:
|
|
15
|
+
transport: str = "stdio"
|
|
16
|
+
port: int = 3000
|
|
17
|
+
tools: list[str] | None = None
|
|
18
|
+
exclude_tools: list[str] | None = None
|
|
19
|
+
secrets: list[str] | None = None
|
|
20
|
+
env_file: str | None = None
|
|
21
|
+
vault_path: str | None = None
|
|
22
|
+
aws_secret_id: str | None = None
|
|
23
|
+
token: str | None = None
|
|
24
|
+
tool_paths: list[str] | None = None
|
|
25
|
+
skill_paths: list[str] | None = None
|
|
26
|
+
https: bool = False
|
|
27
|
+
self_signed: bool = False
|
|
28
|
+
cert_path: str | None = None
|
|
29
|
+
key_path: str | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _parse_args(params: list[str]) -> McpArgs:
|
|
33
|
+
args = McpArgs()
|
|
34
|
+
i = 0
|
|
35
|
+
|
|
36
|
+
def require_value(flag: str) -> str:
|
|
37
|
+
nonlocal i
|
|
38
|
+
if i + 1 >= len(params) or params[i + 1].startswith("-"):
|
|
39
|
+
print(f"❌ {flag} requires a value", file=sys.stderr)
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
i += 1
|
|
42
|
+
return params[i]
|
|
43
|
+
|
|
44
|
+
while i < len(params):
|
|
45
|
+
flag = params[i]
|
|
46
|
+
match flag:
|
|
47
|
+
case "--transport" | "-t":
|
|
48
|
+
val = require_value("--transport")
|
|
49
|
+
if val not in ("stdio", "http"):
|
|
50
|
+
print('❌ --transport must be "stdio" or "http"', file=sys.stderr)
|
|
51
|
+
sys.exit(1)
|
|
52
|
+
args.transport = val
|
|
53
|
+
case "--port" | "-p":
|
|
54
|
+
val = require_value("--port")
|
|
55
|
+
try:
|
|
56
|
+
args.port = int(val)
|
|
57
|
+
except ValueError:
|
|
58
|
+
print("❌ --port must be a number", file=sys.stderr)
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
case "--tools":
|
|
61
|
+
args.tools = [s.strip() for s in require_value("--tools").split(",")]
|
|
62
|
+
case "--exclude":
|
|
63
|
+
args.exclude_tools = [s.strip() for s in require_value("--exclude").split(",")]
|
|
64
|
+
case "--secrets":
|
|
65
|
+
args.secrets = [s.strip() for s in require_value("--secrets").split(",")]
|
|
66
|
+
case "--env-file":
|
|
67
|
+
args.env_file = require_value("--env-file")
|
|
68
|
+
case "--vault-path":
|
|
69
|
+
args.vault_path = require_value("--vault-path")
|
|
70
|
+
case "--aws-secret-id":
|
|
71
|
+
args.aws_secret_id = require_value("--aws-secret-id")
|
|
72
|
+
case "--token":
|
|
73
|
+
args.token = require_value("--token")
|
|
74
|
+
case "--tool-paths":
|
|
75
|
+
args.tool_paths = [s.strip() for s in require_value("--tool-paths").split(",")]
|
|
76
|
+
case "--skill-paths":
|
|
77
|
+
args.skill_paths = [s.strip() for s in require_value("--skill-paths").split(",")]
|
|
78
|
+
case "--https":
|
|
79
|
+
args.https = True
|
|
80
|
+
case "--self-signed":
|
|
81
|
+
args.https = True
|
|
82
|
+
args.self_signed = True
|
|
83
|
+
case "--cert":
|
|
84
|
+
args.cert_path = require_value("--cert")
|
|
85
|
+
args.https = True
|
|
86
|
+
case "--key":
|
|
87
|
+
args.key_path = require_value("--key")
|
|
88
|
+
args.https = True
|
|
89
|
+
case "setup":
|
|
90
|
+
pass # handled separately
|
|
91
|
+
case _:
|
|
92
|
+
if flag.startswith("-"):
|
|
93
|
+
print(f"❌ Unknown flag: {flag}", file=sys.stderr)
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
i += 1
|
|
96
|
+
|
|
97
|
+
return args
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _build_resolver_config(args: McpArgs) -> object:
|
|
101
|
+
"""Build a SecretResolverChain from CLI args."""
|
|
102
|
+
from matimo.mcp.secrets import (
|
|
103
|
+
EnvSecretResolver,
|
|
104
|
+
DotenvSecretResolver,
|
|
105
|
+
VaultSecretResolver,
|
|
106
|
+
AwsSecretsManagerResolver,
|
|
107
|
+
SecretResolverChain,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
secret_types = args.secrets or ["env", "dotenv"]
|
|
111
|
+
resolvers: list[object] = []
|
|
112
|
+
|
|
113
|
+
for t in secret_types:
|
|
114
|
+
match t:
|
|
115
|
+
case "env":
|
|
116
|
+
resolvers.append(EnvSecretResolver())
|
|
117
|
+
case "dotenv":
|
|
118
|
+
resolvers.append(DotenvSecretResolver(path=args.env_file or ".env"))
|
|
119
|
+
case "vault":
|
|
120
|
+
if args.vault_path:
|
|
121
|
+
resolvers.append(VaultSecretResolver(secret_path=args.vault_path))
|
|
122
|
+
case "aws":
|
|
123
|
+
if args.aws_secret_id:
|
|
124
|
+
resolvers.append(AwsSecretsManagerResolver(secret_id=args.aws_secret_id))
|
|
125
|
+
case _:
|
|
126
|
+
print("❌ Unknown secret resolver type. Use: env, dotenv, vault, aws", file=sys.stderr)
|
|
127
|
+
sys.exit(1)
|
|
128
|
+
|
|
129
|
+
# Return None if no resolvers (let MCP server handle it) or a chain if we have resolvers
|
|
130
|
+
return SecretResolverChain(resolvers) if resolvers else None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def mcp_command(params: list[str]) -> None:
|
|
134
|
+
# Handle 'setup' subcommand
|
|
135
|
+
if params and params[0] == "setup":
|
|
136
|
+
from matimo_cli.commands.mcp_setup import mcp_setup_command
|
|
137
|
+
mcp_setup_command()
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
import asyncio
|
|
141
|
+
asyncio.run(_mcp_command_async(params))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
async def _mcp_command_async(params: list[str]) -> None:
|
|
145
|
+
"""Async implementation of the MCP server command."""
|
|
146
|
+
import os
|
|
147
|
+
import sysconfig
|
|
148
|
+
|
|
149
|
+
args = _parse_args(params)
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
from matimo import Matimo
|
|
153
|
+
from matimo.mcp import MCPServer, MCPServerOptions # type: ignore[import-not-found]
|
|
154
|
+
except ImportError as e:
|
|
155
|
+
print("❌ matimo MCP server module not available.", file=sys.stderr)
|
|
156
|
+
print(" Make sure matimo is installed: pip install matimo", file=sys.stderr)
|
|
157
|
+
print(f" Import error: {e}", file=sys.stderr)
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
|
|
160
|
+
# Discover all matimo_* packages if no tool paths provided
|
|
161
|
+
tool_paths = args.tool_paths or []
|
|
162
|
+
if not tool_paths:
|
|
163
|
+
site_packages = sysconfig.get_path("purelib")
|
|
164
|
+
if site_packages and os.path.exists(site_packages):
|
|
165
|
+
for entry in os.listdir(site_packages):
|
|
166
|
+
if entry.startswith("matimo_") and not entry.endswith(".dist-info"):
|
|
167
|
+
pkg_tools = os.path.join(site_packages, entry, "tools")
|
|
168
|
+
if os.path.exists(pkg_tools):
|
|
169
|
+
tool_paths.append(pkg_tools)
|
|
170
|
+
|
|
171
|
+
# Initialize Matimo with the given tool paths
|
|
172
|
+
matimo = await Matimo.init(
|
|
173
|
+
tool_paths=tool_paths if tool_paths else None,
|
|
174
|
+
skill_paths=args.skill_paths,
|
|
175
|
+
auto_discover=True,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Create MCP server options
|
|
179
|
+
options = MCPServerOptions(
|
|
180
|
+
transport=args.transport,
|
|
181
|
+
port=args.port,
|
|
182
|
+
tools=args.tools,
|
|
183
|
+
exclude_tools=args.exclude_tools,
|
|
184
|
+
secret_resolver=_build_resolver_config(args),
|
|
185
|
+
mcp_token=args.token,
|
|
186
|
+
tool_paths=tool_paths if tool_paths else None,
|
|
187
|
+
skill_paths=args.skill_paths,
|
|
188
|
+
auto_discover=True,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Create MCP server
|
|
192
|
+
server = MCPServer(matimo, options)
|
|
193
|
+
|
|
194
|
+
def _shutdown(signum: int, frame: object) -> None:
|
|
195
|
+
if args.transport == "stdio":
|
|
196
|
+
sys.stderr.write("\nShutting down Matimo MCP server…\n")
|
|
197
|
+
else:
|
|
198
|
+
print("\nShutting down Matimo MCP server…")
|
|
199
|
+
sys.exit(0)
|
|
200
|
+
|
|
201
|
+
signal.signal(signal.SIGINT, _shutdown)
|
|
202
|
+
signal.signal(signal.SIGTERM, _shutdown)
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
await server.start()
|
|
206
|
+
|
|
207
|
+
if args.transport == "http":
|
|
208
|
+
protocol = "https" if args.https else "http"
|
|
209
|
+
url = f"{protocol}://localhost:{args.port}/mcp"
|
|
210
|
+
print(f"\n🚀 Matimo MCP server running at {url}")
|
|
211
|
+
if args.https:
|
|
212
|
+
print("🔒 HTTPS enabled")
|
|
213
|
+
print("\n Press Ctrl+C to stop\n")
|
|
214
|
+
except Exception as exc:
|
|
215
|
+
print(f"❌ Failed to start MCP server: {exc}", file=sys.stderr)
|
|
216
|
+
sys.exit(1)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
``matimo mcp setup`` — generate MCP config for Claude Desktop / Cursor.
|
|
3
|
+
|
|
4
|
+
Mirrors: packages/cli/src/commands/mcp-setup.ts
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
_AUTH_PATTERNS = {"TOKEN", "SECRET", "KEY", "PASSWORD", "CREDENTIAL", "AUTH"}
|
|
14
|
+
_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _is_auth_var(name: str) -> bool:
|
|
18
|
+
upper = name.upper()
|
|
19
|
+
return any(p in upper for p in _AUTH_PATTERNS)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def mcp_setup_command() -> None:
|
|
23
|
+
print("\n🔨 Matimo MCP Setup\n")
|
|
24
|
+
print("Scanning for installed tool packages…\n")
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from matimo.core.tool_loader import ToolLoader # type: ignore[import-not-found]
|
|
28
|
+
except ImportError:
|
|
29
|
+
print("❌ matimo core not available. Install it first: pip install matimo", file=sys.stderr)
|
|
30
|
+
sys.exit(1)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
loader = ToolLoader()
|
|
34
|
+
tool_paths = loader.auto_discover_packages()
|
|
35
|
+
|
|
36
|
+
if not tool_paths:
|
|
37
|
+
print("No matimo-* tool packages found.")
|
|
38
|
+
print("Install tools first: matimo install slack github\n")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
tools = loader.load_tools_from_multiple_paths(tool_paths)
|
|
42
|
+
print(f"Found {len(tools)} tools across {len(tool_paths)} package(s):\n")
|
|
43
|
+
|
|
44
|
+
# Group by provider
|
|
45
|
+
providers: dict[str, list[str]] = {}
|
|
46
|
+
auth_vars: set[str] = set()
|
|
47
|
+
|
|
48
|
+
for name, tool in tools.items():
|
|
49
|
+
provider = name.split("_")[0] if "_" in name else name.split("-")[0] if "-" in name else "core"
|
|
50
|
+
providers.setdefault(provider, []).append(name)
|
|
51
|
+
|
|
52
|
+
# Extract auth placeholders
|
|
53
|
+
exec_cfg = tool.execution
|
|
54
|
+
raw = str(exec_cfg.model_dump()) if hasattr(exec_cfg, "model_dump") else str(exec_cfg)
|
|
55
|
+
for m in _PLACEHOLDER_RE.finditer(raw):
|
|
56
|
+
if _is_auth_var(m.group(1)):
|
|
57
|
+
auth_vars.add(m.group(1))
|
|
58
|
+
|
|
59
|
+
for provider, tool_names in sorted(providers.items()):
|
|
60
|
+
print(f" 📦 {provider} ({len(tool_names)} tools)")
|
|
61
|
+
for tn in tool_names[:5]:
|
|
62
|
+
print(f" • {tn}")
|
|
63
|
+
if len(tool_names) > 5:
|
|
64
|
+
print(f" … and {len(tool_names) - 5} more")
|
|
65
|
+
|
|
66
|
+
print()
|
|
67
|
+
|
|
68
|
+
# Display required env vars
|
|
69
|
+
if auth_vars:
|
|
70
|
+
print("🔐 Required environment variables:\n")
|
|
71
|
+
for v in sorted(auth_vars):
|
|
72
|
+
value = os.environ.get(v) or os.environ.get(f"MATIMO_{v}")
|
|
73
|
+
status = "✅" if value else "❌"
|
|
74
|
+
print(f" {status} {v}")
|
|
75
|
+
print()
|
|
76
|
+
|
|
77
|
+
# Generate configs — always use placeholders to prevent secret leakage
|
|
78
|
+
env_block = {v: "<your-token>" for v in sorted(auth_vars)}
|
|
79
|
+
|
|
80
|
+
claude_config = {
|
|
81
|
+
"mcpServers": {
|
|
82
|
+
"matimo": {
|
|
83
|
+
"command": "matimo",
|
|
84
|
+
"args": ["mcp"],
|
|
85
|
+
"env": env_block,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
print("📋 Claude Desktop config (paste into Settings → Developer → MCP Servers):\n")
|
|
91
|
+
print(json.dumps(claude_config, indent=2))
|
|
92
|
+
|
|
93
|
+
print("\n📋 Cursor config (paste into .cursor/mcp.json):\n")
|
|
94
|
+
print(json.dumps(claude_config, indent=2))
|
|
95
|
+
|
|
96
|
+
print("\n📋 HTTP mode (for remote hosting / Docker):\n")
|
|
97
|
+
env_lines = "\n".join(f" {v}=<your-token>" for v in sorted(auth_vars))
|
|
98
|
+
print(f"{env_lines}")
|
|
99
|
+
print(" MATIMO_MCP_TOKEN=<your-server-secret>")
|
|
100
|
+
print(" matimo mcp --transport http --port 3000\n")
|
|
101
|
+
|
|
102
|
+
except Exception as exc:
|
|
103
|
+
print(f"❌ Setup failed: {exc}", file=sys.stderr)
|
|
104
|
+
sys.exit(1)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
``matimo review`` — human oversight for agent-created tools.
|
|
3
|
+
|
|
4
|
+
Mirrors: packages/cli/src/commands/review.ts
|
|
5
|
+
|
|
6
|
+
Subcommands:
|
|
7
|
+
matimo review list Show all tools awaiting approval
|
|
8
|
+
matimo review approve <name> Approve a pending tool
|
|
9
|
+
matimo review reject <name> Reject / revoke a tool
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import yaml
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _resolve_manifest_dir() -> str:
|
|
21
|
+
tool_dir = os.environ.get("MATIMO_TOOL_DIR")
|
|
22
|
+
return str(Path(tool_dir).resolve()) if tool_dir else os.getcwd()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _try_load_manifest(directory: str):
|
|
26
|
+
"""Try to import ApprovalManifest from matimo core."""
|
|
27
|
+
try:
|
|
28
|
+
from matimo.policy.approval_manifest import ApprovalManifest # type: ignore[import-not-found]
|
|
29
|
+
return ApprovalManifest(directory)
|
|
30
|
+
except ImportError:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _print_table(headers: list[str], rows: list[list[str]]) -> None:
|
|
35
|
+
cols = len(headers)
|
|
36
|
+
widths = [
|
|
37
|
+
max(len(h), *(len(r[i]) if i < len(r) else 0 for r in rows))
|
|
38
|
+
for i, h in enumerate(headers)
|
|
39
|
+
]
|
|
40
|
+
def fmt(row):
|
|
41
|
+
return "│ " + " │ ".join(c.ljust(w) for c, w in zip(row, widths)) + " │"
|
|
42
|
+
|
|
43
|
+
print("┌" + "┬".join("─" * (w + 2) for w in widths) + "┐")
|
|
44
|
+
print(fmt(headers))
|
|
45
|
+
print("├" + "┼".join("─" * (w + 2) for w in widths) + "┤")
|
|
46
|
+
for row in rows:
|
|
47
|
+
padded = (row + [""] * cols)[:cols]
|
|
48
|
+
print(fmt(padded))
|
|
49
|
+
print("└" + "┴".join("─" * (w + 2) for w in widths) + "┘")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _list_pending(directory: str) -> None:
|
|
53
|
+
manifest = _try_load_manifest(directory)
|
|
54
|
+
if manifest is None:
|
|
55
|
+
print("❌ matimo core is not available. Run `pip install matimo` first.", file=sys.stderr)
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
|
|
58
|
+
pending = manifest.get_pending_tools()
|
|
59
|
+
approved = manifest.list_approved()
|
|
60
|
+
|
|
61
|
+
if not pending and not approved:
|
|
62
|
+
print("ℹ️ No tools are pending or approved.")
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
if pending:
|
|
66
|
+
print("\n⏳ Pending approval:\n")
|
|
67
|
+
rows = [[name, "pending", "—", "—"] for name in pending]
|
|
68
|
+
_print_table(["Tool name", "Status", "Approved by", "Approved at"], rows)
|
|
69
|
+
|
|
70
|
+
if approved:
|
|
71
|
+
print("\n✅ Approved tools:\n")
|
|
72
|
+
rows = []
|
|
73
|
+
for name in approved:
|
|
74
|
+
rec = manifest.get_approval(name) or {}
|
|
75
|
+
rows.append([name, "approved", rec.get("approved_by", "—"), rec.get("approved_at", "—")])
|
|
76
|
+
_print_table(["Tool name", "Status", "Approved by", "Approved at"], rows)
|
|
77
|
+
|
|
78
|
+
if pending:
|
|
79
|
+
print('\nRun "matimo review approve <tool-name>" to approve, '
|
|
80
|
+
'or "matimo review reject <tool-name>" to reject.')
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _approve_tool(tool_name: str, directory: str) -> None:
|
|
84
|
+
if not tool_name:
|
|
85
|
+
print("❌ Usage: matimo review approve <tool-name>", file=sys.stderr)
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
|
|
88
|
+
manifest = _try_load_manifest(directory)
|
|
89
|
+
if manifest is None:
|
|
90
|
+
print("❌ matimo core is not available.", file=sys.stderr)
|
|
91
|
+
sys.exit(1)
|
|
92
|
+
|
|
93
|
+
pending = manifest.get_pending_tools()
|
|
94
|
+
if tool_name not in pending:
|
|
95
|
+
approved = manifest.list_approved()
|
|
96
|
+
if tool_name in approved:
|
|
97
|
+
print(f'ℹ️ "{tool_name}" is already approved.')
|
|
98
|
+
return
|
|
99
|
+
print(f'❌ No pending tool named "{tool_name}". Run "matimo review list" to see pending tools.', file=sys.stderr)
|
|
100
|
+
sys.exit(1)
|
|
101
|
+
|
|
102
|
+
secret = os.environ.get("MATIMO_APPROVAL_SECRET")
|
|
103
|
+
if not secret:
|
|
104
|
+
print("❌ MATIMO_APPROVAL_SECRET is not set.", file=sys.stderr)
|
|
105
|
+
print(" Set it to approve tools: export MATIMO_APPROVAL_SECRET=<your-secret>")
|
|
106
|
+
sys.exit(1)
|
|
107
|
+
|
|
108
|
+
yaml_path = Path(directory) / tool_name / "definition.yaml"
|
|
109
|
+
if not yaml_path.is_file():
|
|
110
|
+
print(f'❌ Cannot find definition.yaml for tool "{tool_name}" at:\n {yaml_path}', file=sys.stderr)
|
|
111
|
+
sys.exit(1)
|
|
112
|
+
|
|
113
|
+
content = yaml_path.read_text(encoding="utf-8")
|
|
114
|
+
|
|
115
|
+
# Promote status to "approved"
|
|
116
|
+
try:
|
|
117
|
+
parsed = yaml.safe_load(content) or {}
|
|
118
|
+
if isinstance(parsed, dict) and parsed.get("status") != "approved":
|
|
119
|
+
parsed["status"] = "approved"
|
|
120
|
+
content = yaml.dump(parsed, default_flow_style=False)
|
|
121
|
+
tmp_path = yaml_path.with_suffix(".yaml.tmp")
|
|
122
|
+
tmp_path.write_text(content, encoding="utf-8")
|
|
123
|
+
tmp_path.rename(yaml_path)
|
|
124
|
+
print(" 📝 Updated status: draft → approved in definition.yaml")
|
|
125
|
+
except Exception:
|
|
126
|
+
print("⚠️ Failed to update status in definition.yaml; proceeding with manifest approval only.")
|
|
127
|
+
|
|
128
|
+
hash_val = manifest.compute_hash(content)
|
|
129
|
+
approved_by = os.environ.get("USER") or os.environ.get("USERNAME") or "cli"
|
|
130
|
+
manifest.approve(tool_name, hash_val, approved_by)
|
|
131
|
+
print(f'✅ Tool "{tool_name}" approved.')
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _reject_tool(tool_name: str, directory: str) -> None:
|
|
135
|
+
if not tool_name:
|
|
136
|
+
print("❌ Usage: matimo review reject <tool-name>", file=sys.stderr)
|
|
137
|
+
sys.exit(1)
|
|
138
|
+
|
|
139
|
+
manifest = _try_load_manifest(directory)
|
|
140
|
+
if manifest is None:
|
|
141
|
+
print("❌ matimo core is not available.", file=sys.stderr)
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
|
|
144
|
+
was_approved = manifest.revoke(tool_name)
|
|
145
|
+
pending = manifest.get_pending_tools()
|
|
146
|
+
was_pending = tool_name in pending
|
|
147
|
+
|
|
148
|
+
if not was_approved and not was_pending:
|
|
149
|
+
print(f'ℹ️ No record of tool "{tool_name}". Nothing to reject.')
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
print(f'🗑 Tool "{tool_name}" has been rejected/revoked.')
|
|
153
|
+
if was_approved:
|
|
154
|
+
print(" (Approval signature removed — the tool will be blocked until re-approved.)")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def review_command(args: list[str]) -> None:
|
|
158
|
+
sub = args[0] if args else None
|
|
159
|
+
directory = _resolve_manifest_dir()
|
|
160
|
+
|
|
161
|
+
match sub:
|
|
162
|
+
case "list" | None:
|
|
163
|
+
_list_pending(directory)
|
|
164
|
+
case "approve":
|
|
165
|
+
_approve_tool(args[1] if len(args) > 1 else "", directory)
|
|
166
|
+
case "reject":
|
|
167
|
+
_reject_tool(args[1] if len(args) > 1 else "", directory)
|
|
168
|
+
case _:
|
|
169
|
+
print(f'❌ Unknown review subcommand: "{sub}"', file=sys.stderr)
|
|
170
|
+
print("Usage: matimo review [list|approve|reject] [tool-name]")
|
|
171
|
+
sys.exit(1)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
``matimo search`` — search for available Matimo tool packages.
|
|
3
|
+
|
|
4
|
+
Mirrors: packages/cli/src/commands/search.ts
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def search_command(query: str) -> None:
|
|
14
|
+
if not query:
|
|
15
|
+
print("❌ Error: Please specify a search query", file=sys.stderr)
|
|
16
|
+
print("\nUsage: matimo search <query>")
|
|
17
|
+
print("Example: matimo search slack")
|
|
18
|
+
sys.exit(1)
|
|
19
|
+
|
|
20
|
+
# Try to find packages in the repo first, then fall back to installed
|
|
21
|
+
repo_root = _find_repo_root(Path.cwd())
|
|
22
|
+
available: list[dict[str, object]] = []
|
|
23
|
+
context: str
|
|
24
|
+
|
|
25
|
+
if repo_root is not None:
|
|
26
|
+
packages_dir = repo_root / "python" / "providers"
|
|
27
|
+
if packages_dir.is_dir():
|
|
28
|
+
context = "repository"
|
|
29
|
+
available = _scan_directory(packages_dir)
|
|
30
|
+
else:
|
|
31
|
+
packages_dir = repo_root / "packages"
|
|
32
|
+
if packages_dir.is_dir():
|
|
33
|
+
context = "repository"
|
|
34
|
+
available = _scan_directory(packages_dir, skip={"core", "cli"})
|
|
35
|
+
else:
|
|
36
|
+
context = "installed"
|
|
37
|
+
available = _scan_installed()
|
|
38
|
+
else:
|
|
39
|
+
context = "installed"
|
|
40
|
+
available = _scan_installed()
|
|
41
|
+
|
|
42
|
+
if not available:
|
|
43
|
+
print("❌ No Matimo packages found.", file=sys.stderr)
|
|
44
|
+
print("Install packages: pip install matimo-slack")
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
q = query.lower()
|
|
48
|
+
results = [
|
|
49
|
+
p
|
|
50
|
+
for p in available
|
|
51
|
+
if q in str(p["name"]).lower() or q in str(p.get("description", "")).lower()
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
if not results:
|
|
55
|
+
print(f'❌ No packages found matching "{query}"')
|
|
56
|
+
print(f"\n📦 Available Packages (from {context}):")
|
|
57
|
+
for p in available:
|
|
58
|
+
print(f" • {p['name']} ({p['tools']} tools)")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
print(f'🔍 Search results for "{query}" ({context}):\n')
|
|
62
|
+
for p in results:
|
|
63
|
+
print(f"✅ {p['name']}")
|
|
64
|
+
print(f" {p.get('description', '')}")
|
|
65
|
+
print(f" Tools: {p['tools']}")
|
|
66
|
+
if context == "installed":
|
|
67
|
+
print(" Already installed")
|
|
68
|
+
else:
|
|
69
|
+
name_part = str(p["name"]).replace("matimo-", "")
|
|
70
|
+
print(f" Install: matimo install {name_part}")
|
|
71
|
+
print()
|
|
72
|
+
|
|
73
|
+
print(f"Total: {len(results)} package{'s' if len(results) != 1 else ''} found")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# Helpers
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _find_repo_root(start: Path) -> Path | None:
|
|
82
|
+
current = start
|
|
83
|
+
while current != current.parent:
|
|
84
|
+
if (current / "pnpm-workspace.yaml").is_file() or (current / "pyproject.toml").is_file():
|
|
85
|
+
if (current / "python").is_dir() or (current / "packages").is_dir():
|
|
86
|
+
return current
|
|
87
|
+
current = current.parent
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _scan_directory(
|
|
92
|
+
packages_dir: Path,
|
|
93
|
+
skip: set[str] | None = None,
|
|
94
|
+
) -> list[dict[str, object]]:
|
|
95
|
+
results: list[dict[str, object]] = []
|
|
96
|
+
skip = skip or set()
|
|
97
|
+
|
|
98
|
+
for entry in sorted(packages_dir.iterdir()):
|
|
99
|
+
if not entry.is_dir() or entry.name in skip or entry.name.startswith("."):
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
tools_dir = entry / "tools"
|
|
103
|
+
tool_count = 0
|
|
104
|
+
if tools_dir.is_dir():
|
|
105
|
+
tool_count = sum(1 for t in tools_dir.iterdir() if t.is_dir())
|
|
106
|
+
|
|
107
|
+
# Try to extract description from pyproject.toml
|
|
108
|
+
desc = ""
|
|
109
|
+
pyproject = entry / "pyproject.toml"
|
|
110
|
+
if pyproject.is_file():
|
|
111
|
+
try:
|
|
112
|
+
import tomllib
|
|
113
|
+
|
|
114
|
+
data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
|
|
115
|
+
desc = data.get("project", {}).get("description", "")
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
name = entry.name
|
|
120
|
+
results.append({"name": name, "description": desc, "tools": tool_count})
|
|
121
|
+
|
|
122
|
+
return results
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _scan_installed() -> list[dict[str, object]]:
|
|
126
|
+
import importlib.metadata
|
|
127
|
+
|
|
128
|
+
results: list[dict[str, object]] = []
|
|
129
|
+
for dist in importlib.metadata.distributions():
|
|
130
|
+
pkg_name = dist.metadata["Name"]
|
|
131
|
+
if pkg_name and pkg_name.startswith("matimo-") and pkg_name != "matimo-cli":
|
|
132
|
+
results.append(
|
|
133
|
+
{
|
|
134
|
+
"name": pkg_name,
|
|
135
|
+
"description": dist.metadata.get("Summary", ""),
|
|
136
|
+
"tools": 0,
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
return results
|