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.
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/matimo-cli)](https://pypi.org/project/matimo-cli/)
22
+ [![Docs](https://img.shields.io/badge/docs-matimo.dev-blue)](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
+ [![PyPI](https://img.shields.io/pypi/v/matimo-cli)](https://pypi.org/project/matimo-cli/)
6
+ [![Docs](https://img.shields.io/badge/docs-matimo.dev-blue)](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,4 @@
1
+ """Allow running ``python -m matimo_cli``."""
2
+ from matimo_cli.cli import main
3
+
4
+ main()
@@ -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