muscle-memory-mcp 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.
Files changed (32) hide show
  1. muscle_memory_mcp-0.1.0/.gitignore +4 -0
  2. muscle_memory_mcp-0.1.0/LICENSE +21 -0
  3. muscle_memory_mcp-0.1.0/PKG-INFO +134 -0
  4. muscle_memory_mcp-0.1.0/README.md +116 -0
  5. muscle_memory_mcp-0.1.0/pyproject.toml +38 -0
  6. muscle_memory_mcp-0.1.0/server.json +20 -0
  7. muscle_memory_mcp-0.1.0/src/muscle_memory_mcp/__init__.py +3 -0
  8. muscle_memory_mcp-0.1.0/src/muscle_memory_mcp/__main__.py +5 -0
  9. muscle_memory_mcp-0.1.0/src/muscle_memory_mcp/cli.py +138 -0
  10. muscle_memory_mcp-0.1.0/src/muscle_memory_mcp/core.py +158 -0
  11. muscle_memory_mcp-0.1.0/src/muscle_memory_mcp/index.py +47 -0
  12. muscle_memory_mcp-0.1.0/src/muscle_memory_mcp/onboarding.py +159 -0
  13. muscle_memory_mcp-0.1.0/src/muscle_memory_mcp/paths.py +68 -0
  14. muscle_memory_mcp-0.1.0/src/muscle_memory_mcp/runners.py +142 -0
  15. muscle_memory_mcp-0.1.0/src/muscle_memory_mcp/server.py +203 -0
  16. muscle_memory_mcp-0.1.0/src/muscle_memory_mcp/store.py +376 -0
  17. muscle_memory_mcp-0.1.0/src/muscle_memory_mcp/testing.py +59 -0
  18. muscle_memory_mcp-0.1.0/src/muscle_memory_mcp/trust.py +40 -0
  19. muscle_memory_mcp-0.1.0/tests/test_cli.py +82 -0
  20. muscle_memory_mcp-0.1.0/tests/test_core.py +348 -0
  21. muscle_memory_mcp-0.1.0/tests/test_docs.py +152 -0
  22. muscle_memory_mcp-0.1.0/tests/test_e2e.py +146 -0
  23. muscle_memory_mcp-0.1.0/tests/test_hardening.py +535 -0
  24. muscle_memory_mcp-0.1.0/tests/test_libraries.py +227 -0
  25. muscle_memory_mcp-0.1.0/tests/test_onboarding.py +195 -0
  26. muscle_memory_mcp-0.1.0/tests/test_packaging.py +58 -0
  27. muscle_memory_mcp-0.1.0/tests/test_release_workflow.py +30 -0
  28. muscle_memory_mcp-0.1.0/tests/test_runners.py +53 -0
  29. muscle_memory_mcp-0.1.0/tests/test_server.py +206 -0
  30. muscle_memory_mcp-0.1.0/tests/test_store.py +312 -0
  31. muscle_memory_mcp-0.1.0/tests/test_testing.py +188 -0
  32. muscle_memory_mcp-0.1.0/tests/test_trust.py +177 -0
@@ -0,0 +1,4 @@
1
+ __pycache__/
2
+ *.pyc
3
+ dist/
4
+ .venv/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Johannes Loor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.5
2
+ Name: muscle-memory-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server that gives your agent a library of its own tested, reusable scripts.
5
+ Project-URL: Homepage, https://github.com/johannesloor/muscle-memory
6
+ Project-URL: Repository, https://github.com/johannesloor/muscle-memory
7
+ Project-URL: Issues, https://github.com/johannesloor/muscle-memory/issues
8
+ Author: Johannes Loor
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai-agents,automation,claude-code,codex,cursor,mcp,mcp-server,model-context-protocol
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+
19
+ ![A tied shoelace bow next to the words Muscle Memory MCP Server](https://raw.githubusercontent.com/johannesloor/muscle-memory/main/assets/banner.png)
20
+
21
+ # Muscle Memory MCP Server
22
+
23
+ [![PyPI](https://img.shields.io/pypi/v/muscle-memory-mcp?style=flat-square)](https://pypi.org/project/muscle-memory-mcp/) [![Tests](https://img.shields.io/github/actions/workflow/status/johannesloor/muscle-memory/ci.yml?branch=main&style=flat-square&label=tests)](https://github.com/johannesloor/muscle-memory/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
24
+
25
+ <!-- mcp-name: io.github.johannesloor/muscle-memory -->
26
+
27
+ Your agent solves problems by writing scripts, then throws them away like they're nothing. Tomorrow it writes the same script again. Like having to figure out how to tie your shoelaces every morning. It lacks muscle memory.
28
+
29
+ The Muscle Memory MCP server gives your agent an empty library that it fills with its own tested scripts.
30
+
31
+ ## Install
32
+
33
+ [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=muscle-memory&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22muscle-memory-mcp%22%5D%7D) [![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=muscle-memory&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22muscle-memory-mcp%22%5D%7D&quality=insiders) [![Install in Cursor](https://img.shields.io/badge/Cursor-Install_Server-000000?style=flat-square&logo=cursor&logoColor=white)](https://cursor.com/en/install-mcp?name=muscle-memory&config=eyJjb21tYW5kIjoidXZ4IiwiYXJncyI6WyJtdXNjbGUtbWVtb3J5LW1jcCJdfQ%3D%3D) [![Install in LM Studio](https://img.shields.io/badge/LM_Studio-Install_Server-4F46E5?style=flat-square)](https://lmstudio.ai/install-mcp?name=muscle-memory&config=eyJjb21tYW5kIjoidXZ4IiwiYXJncyI6WyJtdXNjbGUtbWVtb3J5LW1jcCJdfQ%3D%3D)
34
+
35
+ **Hermes**
36
+
37
+ ```sh
38
+ hermes mcp add muscle-memory --command uvx --args muscle-memory-mcp
39
+ ```
40
+
41
+ **Claude Code**
42
+
43
+ ```sh
44
+ claude mcp add muscle-memory -s user -- uvx muscle-memory-mcp
45
+ ```
46
+
47
+ To keep the library somewhere other than the default, set `MUSCLE_MEMORY_DIR` in the server's environment.
48
+
49
+ ### Codex
50
+
51
+ Add to `~/.codex/config.toml`:
52
+
53
+ ```toml
54
+ [mcp_servers.muscle-memory]
55
+ command = "uvx"
56
+ args = ["muscle-memory-mcp"]
57
+ # env = { MUSCLE_MEMORY_DIR = "/absolute/path/to/library" }
58
+ ```
59
+
60
+ ### Cursor
61
+
62
+ Add to `~/.cursor/mcp.json`:
63
+
64
+ ```json
65
+ {
66
+ "mcpServers": {
67
+ "muscle-memory": {
68
+ "command": "uvx",
69
+ "args": ["muscle-memory-mcp"]
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+
76
+ ## Add to your AGENTS.md
77
+
78
+ Add this standing rule to your `AGENTS.md`, `CLAUDE.md`, `SOUL.md` or equivalent agent instructions:
79
+
80
+ ```text
81
+ Before writing helper code, check muscle-memory with mm_list and run a match with mm_run.
82
+ After giving the user the result, save reusable new helpers with mm_save.
83
+ Skip this for code that is itself the deliverable.
84
+ ```
85
+
86
+ Tool descriptions contain the safety and testing specifics for each operation.
87
+
88
+ ## First run and storage
89
+
90
+ The server exposes `mm_list`, `mm_run`, `mm_save` and `mm_setup`. If the personal library has not been set up, the tools guide the agent to ask you once where it should live and then call `mm_setup`.
91
+
92
+ The personal library defaults to `~/.muscle-memory` (`%USERPROFILE%\.muscle-memory` on Windows); `MUSCLE_MEMORY_DIR` selects another location. Scripts in the personal library are available across projects.
93
+
94
+ An optional `.muscle-memory/` project library holds scripts specific to one codebase. Project-root detection depends on the MCP client's working directory. Set `MUSCLE_MEMORY_PROJECT_DIR` to the project's absolute path in the server environment to override detection explicitly. When a script name exists in both libraries, the project copy takes precedence; listings mark the personal copy as shadowed.
95
+
96
+ For personal libraries, saves write files locally if the folder is not in git. If the library is a git repo, each saved script is committed. Setup asks whether to also push scripts to the git remote automatically; run setup again to change your answer. Project libraries are never committed or pushed automatically: you commit them with your own work. On first project setup, the agent asks whether the library should be shared through git or kept private.
97
+
98
+ ## Tools
99
+
100
+ - **`mm_list(area?)`** — list available areas and script counts, or list scripts in one area.
101
+ - **`mm_run(script, args)`** — run a saved script, passing through arguments.
102
+ - **`mm_save(name, code, test, area, usage, ...)`** — save or update a script after its test passes. Supports runner, test selection, and personal/project scope options; see the tool description for the full schema and safety behavior.
103
+ - **`mm_setup(target, ...)`** — set up a personal library or optional project library. Ask the user before setup; see the tool description for modes and options.
104
+
105
+ ## Advanced: languages and tests
106
+
107
+ The runner is picked by file extension: Python, bash/zsh, Node, Ruby, Perl, PHP, Swift, Go, Lua, R, PowerShell, and `.cmd`/`.bat` on Windows. Other languages need an explicit runner argv when saved. Runtimes are never installed for you; a missing one gives an error naming the command.
108
+
109
+ By default a script's test uses the same extension and runner. `test_name` and `test_runner` allow independent test selection. Tests live under `tests/test_<stem>.<ext>`; selection metadata is stored beside the script. Tests receive `MM_SCRIPT` (absolute script path) and `MM_LIBRARY` (library root). Exit status 0 passes; failed tests prevent saving. Replacing a test extension removes the prior test only after the replacement passes.
110
+
111
+ ## Advanced: CLI
112
+
113
+ The same command is also a CLI:
114
+
115
+ ```sh
116
+ uvx muscle-memory-mcp list [AREA]
117
+ uvx muscle-memory-mcp run SCRIPT [ARGS...]
118
+ uvx muscle-memory-mcp save NAME --code FILE --test FILE --area AREA --usage TEXT
119
+ uvx muscle-memory-mcp setup [--target personal|project] [OPTIONS]
120
+ uvx muscle-memory-mcp test [NAME]
121
+ uvx muscle-memory-mcp trust --project
122
+ ```
123
+
124
+ With no arguments it starts the MCP server over stdio. `--help` lists the options.
125
+
126
+ ## Security and limits
127
+
128
+ - Scripts and tests run as you, with your environment and no sandbox. Passing tests is not a security review.
129
+ - A project library runs only after you approve the project once with `uvx muscle-memory-mcp trust --project`.
130
+ - Project-library detection depends on the client's working directory; set `MUSCLE_MEMORY_PROJECT_DIR` if needed.
131
+
132
+ ## License
133
+
134
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,116 @@
1
+ ![A tied shoelace bow next to the words Muscle Memory MCP Server](https://raw.githubusercontent.com/johannesloor/muscle-memory/main/assets/banner.png)
2
+
3
+ # Muscle Memory MCP Server
4
+
5
+ [![PyPI](https://img.shields.io/pypi/v/muscle-memory-mcp?style=flat-square)](https://pypi.org/project/muscle-memory-mcp/) [![Tests](https://img.shields.io/github/actions/workflow/status/johannesloor/muscle-memory/ci.yml?branch=main&style=flat-square&label=tests)](https://github.com/johannesloor/muscle-memory/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
6
+
7
+ <!-- mcp-name: io.github.johannesloor/muscle-memory -->
8
+
9
+ Your agent solves problems by writing scripts, then throws them away like they're nothing. Tomorrow it writes the same script again. Like having to figure out how to tie your shoelaces every morning. It lacks muscle memory.
10
+
11
+ The Muscle Memory MCP server gives your agent an empty library that it fills with its own tested scripts.
12
+
13
+ ## Install
14
+
15
+ [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=muscle-memory&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22muscle-memory-mcp%22%5D%7D) [![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=muscle-memory&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22muscle-memory-mcp%22%5D%7D&quality=insiders) [![Install in Cursor](https://img.shields.io/badge/Cursor-Install_Server-000000?style=flat-square&logo=cursor&logoColor=white)](https://cursor.com/en/install-mcp?name=muscle-memory&config=eyJjb21tYW5kIjoidXZ4IiwiYXJncyI6WyJtdXNjbGUtbWVtb3J5LW1jcCJdfQ%3D%3D) [![Install in LM Studio](https://img.shields.io/badge/LM_Studio-Install_Server-4F46E5?style=flat-square)](https://lmstudio.ai/install-mcp?name=muscle-memory&config=eyJjb21tYW5kIjoidXZ4IiwiYXJncyI6WyJtdXNjbGUtbWVtb3J5LW1jcCJdfQ%3D%3D)
16
+
17
+ **Hermes**
18
+
19
+ ```sh
20
+ hermes mcp add muscle-memory --command uvx --args muscle-memory-mcp
21
+ ```
22
+
23
+ **Claude Code**
24
+
25
+ ```sh
26
+ claude mcp add muscle-memory -s user -- uvx muscle-memory-mcp
27
+ ```
28
+
29
+ To keep the library somewhere other than the default, set `MUSCLE_MEMORY_DIR` in the server's environment.
30
+
31
+ ### Codex
32
+
33
+ Add to `~/.codex/config.toml`:
34
+
35
+ ```toml
36
+ [mcp_servers.muscle-memory]
37
+ command = "uvx"
38
+ args = ["muscle-memory-mcp"]
39
+ # env = { MUSCLE_MEMORY_DIR = "/absolute/path/to/library" }
40
+ ```
41
+
42
+ ### Cursor
43
+
44
+ Add to `~/.cursor/mcp.json`:
45
+
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "muscle-memory": {
50
+ "command": "uvx",
51
+ "args": ["muscle-memory-mcp"]
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+
58
+ ## Add to your AGENTS.md
59
+
60
+ Add this standing rule to your `AGENTS.md`, `CLAUDE.md`, `SOUL.md` or equivalent agent instructions:
61
+
62
+ ```text
63
+ Before writing helper code, check muscle-memory with mm_list and run a match with mm_run.
64
+ After giving the user the result, save reusable new helpers with mm_save.
65
+ Skip this for code that is itself the deliverable.
66
+ ```
67
+
68
+ Tool descriptions contain the safety and testing specifics for each operation.
69
+
70
+ ## First run and storage
71
+
72
+ The server exposes `mm_list`, `mm_run`, `mm_save` and `mm_setup`. If the personal library has not been set up, the tools guide the agent to ask you once where it should live and then call `mm_setup`.
73
+
74
+ The personal library defaults to `~/.muscle-memory` (`%USERPROFILE%\.muscle-memory` on Windows); `MUSCLE_MEMORY_DIR` selects another location. Scripts in the personal library are available across projects.
75
+
76
+ An optional `.muscle-memory/` project library holds scripts specific to one codebase. Project-root detection depends on the MCP client's working directory. Set `MUSCLE_MEMORY_PROJECT_DIR` to the project's absolute path in the server environment to override detection explicitly. When a script name exists in both libraries, the project copy takes precedence; listings mark the personal copy as shadowed.
77
+
78
+ For personal libraries, saves write files locally if the folder is not in git. If the library is a git repo, each saved script is committed. Setup asks whether to also push scripts to the git remote automatically; run setup again to change your answer. Project libraries are never committed or pushed automatically: you commit them with your own work. On first project setup, the agent asks whether the library should be shared through git or kept private.
79
+
80
+ ## Tools
81
+
82
+ - **`mm_list(area?)`** — list available areas and script counts, or list scripts in one area.
83
+ - **`mm_run(script, args)`** — run a saved script, passing through arguments.
84
+ - **`mm_save(name, code, test, area, usage, ...)`** — save or update a script after its test passes. Supports runner, test selection, and personal/project scope options; see the tool description for the full schema and safety behavior.
85
+ - **`mm_setup(target, ...)`** — set up a personal library or optional project library. Ask the user before setup; see the tool description for modes and options.
86
+
87
+ ## Advanced: languages and tests
88
+
89
+ The runner is picked by file extension: Python, bash/zsh, Node, Ruby, Perl, PHP, Swift, Go, Lua, R, PowerShell, and `.cmd`/`.bat` on Windows. Other languages need an explicit runner argv when saved. Runtimes are never installed for you; a missing one gives an error naming the command.
90
+
91
+ By default a script's test uses the same extension and runner. `test_name` and `test_runner` allow independent test selection. Tests live under `tests/test_<stem>.<ext>`; selection metadata is stored beside the script. Tests receive `MM_SCRIPT` (absolute script path) and `MM_LIBRARY` (library root). Exit status 0 passes; failed tests prevent saving. Replacing a test extension removes the prior test only after the replacement passes.
92
+
93
+ ## Advanced: CLI
94
+
95
+ The same command is also a CLI:
96
+
97
+ ```sh
98
+ uvx muscle-memory-mcp list [AREA]
99
+ uvx muscle-memory-mcp run SCRIPT [ARGS...]
100
+ uvx muscle-memory-mcp save NAME --code FILE --test FILE --area AREA --usage TEXT
101
+ uvx muscle-memory-mcp setup [--target personal|project] [OPTIONS]
102
+ uvx muscle-memory-mcp test [NAME]
103
+ uvx muscle-memory-mcp trust --project
104
+ ```
105
+
106
+ With no arguments it starts the MCP server over stdio. `--help` lists the options.
107
+
108
+ ## Security and limits
109
+
110
+ - Scripts and tests run as you, with your environment and no sandbox. Passing tests is not a security review.
111
+ - A project library runs only after you approve the project once with `uvx muscle-memory-mcp trust --project`.
112
+ - Project-library detection depends on the client's working directory; set `MUSCLE_MEMORY_PROJECT_DIR` if needed.
113
+
114
+ ## License
115
+
116
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "muscle-memory-mcp"
7
+ dynamic = ["version"]
8
+ description = "MCP server that gives your agent a library of its own tested, reusable scripts."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "Johannes Loor" }]
13
+ dependencies = []
14
+ keywords = ["mcp", "mcp-server", "model-context-protocol", "ai-agents", "claude-code", "codex",
15
+ "cursor", "automation"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Topic :: Software Development",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/johannesloor/muscle-memory"
25
+ Repository = "https://github.com/johannesloor/muscle-memory"
26
+ Issues = "https://github.com/johannesloor/muscle-memory/issues"
27
+
28
+ [project.scripts]
29
+ muscle-memory-mcp = "muscle_memory_mcp.cli:main"
30
+
31
+ [tool.hatch.version]
32
+ path = "src/muscle_memory_mcp/__init__.py"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/muscle_memory_mcp"]
36
+
37
+ [tool.hatch.build.targets.sdist]
38
+ include = ["src", "tests", "README.md", "LICENSE", "pyproject.toml", "server.json"]
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.johannesloor/muscle-memory",
4
+ "description": "MCP server that gives your agent a library of its own tested, reusable scripts.",
5
+ "version": "0.1.0",
6
+ "repository": {
7
+ "url": "https://github.com/johannesloor/muscle-memory",
8
+ "source": "github"
9
+ },
10
+ "packages": [
11
+ {
12
+ "registryType": "pypi",
13
+ "identifier": "muscle-memory-mcp",
14
+ "version": "0.1.0",
15
+ "transport": {
16
+ "type": "stdio"
17
+ }
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,3 @@
1
+ """muscle-memory-mcp: a personal library of tested, reusable scripts exposed over MCP."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from . import cli
4
+
5
+ sys.exit(cli.main())
@@ -0,0 +1,138 @@
1
+ """Command-line interface for muscle-memory-mcp.
2
+
3
+ muscle-memory-mcp run the MCP server (stdio)
4
+ muscle-memory-mcp list [AREA] without AREA: all areas with script counts; with AREA: its scripts
5
+ muscle-memory-mcp run SCRIPT [ARGS...] run a library script, passing ARGS through
6
+ muscle-memory-mcp save NAME --code F --test F --area AREA --usage TEXT [--new-area] [--no-push]
7
+ add a script + test, run the suite, index, commit
8
+ muscle-memory-mcp setup [--target personal|project] [options] set up a library
9
+ muscle-memory-mcp test [NAME] without NAME: run every script's own test; with NAME: just that one
10
+ """
11
+ import argparse
12
+ import json
13
+ import subprocess
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ from . import core, onboarding, paths, server, testing
18
+
19
+
20
+ def main(argv=None):
21
+ if argv is None:
22
+ argv = sys.argv[1:]
23
+ for stream in (sys.stdout, sys.stderr): # scripts may print any Unicode; never crash on a code page
24
+ if hasattr(stream, "reconfigure"):
25
+ stream.reconfigure(encoding="utf-8")
26
+ if not argv:
27
+ server.main()
28
+ return 0
29
+ ap = argparse.ArgumentParser(prog="muscle-memory-mcp", description=__doc__,
30
+ formatter_class=argparse.RawDescriptionHelpFormatter)
31
+ sub = ap.add_subparsers(dest="cmd", required=True)
32
+ s = sub.add_parser("list"); s.add_argument("area", nargs="*")
33
+ r = sub.add_parser("run"); r.add_argument("script"); r.add_argument("args", nargs=argparse.REMAINDER)
34
+ v = sub.add_parser("save"); v.add_argument("name")
35
+ v.add_argument("--code", required=True); v.add_argument("--test", required=True)
36
+ v.add_argument("--area", required=True); v.add_argument("--usage", required=True)
37
+ v.add_argument("--scope", choices=("personal", "project"), default="personal")
38
+ v.add_argument("--replace", action="store_true")
39
+ push_options = v.add_mutually_exclusive_group()
40
+ push_options.add_argument("--push", action="store_true", help="push this save even if setup chose not to")
41
+ push_options.add_argument("--no-push", action="store_true", help="don't push this save even if setup chose to")
42
+ v.add_argument("--new-area", action="store_true", help="allow creating AREA if it does not exist")
43
+ v.add_argument("--test-name")
44
+ v.add_argument("--test-runner", help="JSON argv for the test interpreter")
45
+ v.add_argument("--runner", help="JSON array argv for a custom interpreter, e.g. '[\"bash\"]' or "
46
+ "'[\"deno\", \"run\"]'; only needed for extensions with no built-in runner")
47
+ setup = sub.add_parser("setup")
48
+ setup.add_argument("--target", choices=("personal", "project"), default="personal")
49
+ setup.add_argument("--mode", choices=("local", "existing", "clone"))
50
+ setup.add_argument("--path"); setup.add_argument("--remote")
51
+ setup.add_argument("--git", action="store_true")
52
+ setup.add_argument("--shared", action=argparse.BooleanOptionalAction, default=None,
53
+ help="project library: --shared commits it with the project, --no-shared keeps it private")
54
+ setup.add_argument("--push", action=argparse.BooleanOptionalAction, default=None,
55
+ help="personal git library: push scripts to the git remote automatically")
56
+ t = sub.add_parser("test"); t.add_argument("name", nargs="?",
57
+ help="one script's file name; omit to test every saved script")
58
+ trust_cmd = sub.add_parser("trust", help="review and explicitly approve a project library")
59
+ trust_cmd.add_argument("--project", action="store_true", required=True)
60
+ a = ap.parse_args(argv)
61
+ try:
62
+ if a.cmd == "trust":
63
+ project = paths.project_root()
64
+ if project is None:
65
+ raise ValueError("no project library here")
66
+ root = (project / ".muscle-memory").resolve()
67
+ if not (root / "INDEX.md").is_file():
68
+ raise ValueError("no project library here")
69
+ from . import trust
70
+ trust.approve(root)
71
+ print(f"Trusted project library {root}.")
72
+ return 0
73
+ if a.cmd == "setup":
74
+ print(server.call("mm_setup", {"target": a.target, "mode": a.mode, "path": a.path,
75
+ "remote": a.remote, "git": a.git, "shared": a.shared,
76
+ "push": a.push})[0])
77
+ return 0
78
+ if a.cmd == "list":
79
+ text = core.list_areas() if not a.area else core.list_area(" ".join(a.area))
80
+ print(text)
81
+ return 3 if text == onboarding.NOT_SET_UP else 0
82
+ if a.cmd == "run":
83
+ code, out, err = core.run(a.script, a.args)
84
+ sys.stdout.write(out); sys.stderr.write(err)
85
+ return code
86
+ if a.cmd == "test":
87
+ if a.name:
88
+ lib, _ = core.resolve(a.name)
89
+ jobs = [(lib, a.name)]
90
+ else:
91
+ jobs = []
92
+ for lib in paths.libraries():
93
+ scripts_dir = lib.root / "scripts"
94
+ if not scripts_dir.exists():
95
+ continue
96
+ names = sorted(p.name for p in scripts_dir.iterdir()
97
+ if p.is_file() and not p.name.startswith("test_")
98
+ and not p.name.endswith(".runner.json") and not p.name.endswith(".test.json"))
99
+ jobs.extend((lib, n) for n in names)
100
+ passed, failed = 0, []
101
+ for lib, n in jobs:
102
+ ok, msg = testing.run_test(lib.root, n)
103
+ if ok:
104
+ passed += 1
105
+ else:
106
+ failed.append(n)
107
+ print(f"FAIL {n}\n{msg}")
108
+ print(f"{passed} passed" + (f", {len(failed)} failed" if failed else ""))
109
+ return 1 if failed else 0
110
+ runner = None
111
+ test_runner = None
112
+ for option, raw in (("--runner", a.runner), ("--test-runner", a.test_runner)):
113
+ if raw:
114
+ try:
115
+ parsed = json.loads(raw)
116
+ except json.JSONDecodeError as e:
117
+ raise ValueError(f"{option} must be a JSON array of strings: {e}")
118
+ if option == "--runner": runner = parsed
119
+ else: test_runner = parsed
120
+ ok, msg = core.save(a.name, Path(a.code).read_text(encoding="utf-8"),
121
+ Path(a.test).read_text(encoding="utf-8"), a.area, a.usage,
122
+ scope=a.scope, new_area=a.new_area, runner=runner,
123
+ test_name=a.test_name, test_runner=test_runner,
124
+ replace=a.replace, push=True if a.push else False if a.no_push else None)
125
+ print(msg)
126
+ if msg == onboarding.NOT_SET_UP:
127
+ return 3
128
+ return 0 if ok else 1
129
+ except (ValueError, OSError, subprocess.SubprocessError) as e:
130
+ if str(e) == onboarding.NOT_SET_UP:
131
+ print(onboarding.NOT_SET_UP)
132
+ return 3
133
+ print(f"error: {e}", file=sys.stderr)
134
+ return 2
135
+
136
+
137
+ if __name__ == "__main__":
138
+ sys.exit(main())
@@ -0,0 +1,158 @@
1
+ """Core library: list, run and save scripts in the muscle-memory library.
2
+
3
+ Personal library dir: $MUSCLE_MEMORY_DIR or ~/.muscle-memory; optional project library at the
4
+ project root's .muscle-memory/ (see paths.py). Both have scripts/ and tests/. This module is
5
+ standard library only. Scripts may be in any language: common ones are picked up by file
6
+ extension (see runners.LANGUAGE_RUNNERS); anything else needs an explicit --runner argv.
7
+ Each script's test runs in the script's own language, via testing.run_test() (see testing.py).
8
+
9
+ list_areas()/list_area()/resolve() see both the personal and (if present) project library,
10
+ labelled by scope, with the project library taking precedence on a name clash. areas()/
11
+ area_lines()/format_areas() are lower-level, single-library helpers. Save behaviour lives in store.py;
12
+ core.save() is retained as a compatibility re-export.
13
+ """
14
+ import os
15
+ from pathlib import Path
16
+
17
+ from . import paths, runners
18
+ from .onboarding import NOT_SET_UP
19
+ from .index import add_index_line, parse_index # noqa: F401 (re-exported; moved from here)
20
+
21
+ def lib_dir():
22
+ return paths.personal_root()
23
+
24
+
25
+ def _read_index_text(root):
26
+ """root's INDEX.md as UTF-8, newline='' preserved (empty string if it doesn't exist).
27
+
28
+ Path.read_text() only gained a newline= parameter in Python 3.13; this module supports
29
+ Python >=3.10, so open() is used directly instead.
30
+ """
31
+ p = Path(root) / "INDEX.md"
32
+ if not p.exists():
33
+ return ""
34
+ with open(p, encoding="utf-8", newline="") as f:
35
+ return f.read()
36
+
37
+
38
+ def read_index():
39
+ return _read_index_text(lib_dir())
40
+
41
+
42
+ def areas(text=None):
43
+ """[(area, script count)] in index order, for a single library's index text."""
44
+ counts = {}
45
+ for e in parse_index(read_index() if text is None else text):
46
+ counts[e["area"]] = counts.get(e["area"], 0) + 1
47
+ return list(counts.items())
48
+
49
+
50
+ def area_lines(area, text=None):
51
+ """All index lines in one area (case-insensitive name); ValueError lists the areas if unknown."""
52
+ text = read_index() if text is None else text
53
+ known = [a for a, _ in areas(text)]
54
+ match = next((a for a in known if a.lower() == area.strip().lower()), None)
55
+ if match is None:
56
+ raise ValueError(f"no area '{area}'. Areas: {', '.join(known) or '(library is empty)'}")
57
+ return match, [e["line"] for e in parse_index(text) if e["area"] == match]
58
+
59
+
60
+ def format_areas(pairs):
61
+ if not pairs:
62
+ return "The library is empty. Do the task, answer, then save a script if the step is reusable."
63
+ return "Areas (script count):\n" + "\n".join(f"- {a} ({n})" for a, n in pairs)
64
+
65
+
66
+ def list_areas():
67
+ """Areas from every visible library (project first), each labelled with its scope."""
68
+ if not paths.personal_root().is_dir():
69
+ return NOT_SET_UP
70
+ triples = [] # (area, count, scope)
71
+ for lib in paths.libraries():
72
+ for area, count in areas(_read_index_text(lib.root)):
73
+ triples.append((area, count, lib.scope))
74
+ if not triples:
75
+ return "The library is empty. Do the task, answer, then save a script if the step is reusable."
76
+ lines = "\n".join(f"- {a} ({n}, {s})" for a, n, s in triples)
77
+ return "Areas (script count):\n" + lines
78
+
79
+
80
+ def list_area(area):
81
+ """One area's lines from every library that has it, each under its own '## Area (scope)'.
82
+
83
+ A personal line whose script also exists in the project library's scripts/ (so mm_run
84
+ would pick the project one, per resolve()'s precedence) is marked '(shadowed)'.
85
+ """
86
+ if not paths.personal_root().is_dir():
87
+ return NOT_SET_UP
88
+ libs = paths.libraries()
89
+ project_lib = next((lib for lib in libs if lib.scope == "project"), None)
90
+ project_scripts = set()
91
+ if project_lib is not None and (project_lib.root / "scripts").is_dir():
92
+ project_scripts = {p.name for p in (project_lib.root / "scripts").iterdir() if p.is_file()}
93
+
94
+ sections, known = [], []
95
+ for lib in libs:
96
+ text = _read_index_text(lib.root)
97
+ lib_known = [a for a, _ in areas(text)]
98
+ known.extend(a for a in lib_known if a not in known)
99
+ match = next((a for a in lib_known if a.lower() == area.strip().lower()), None)
100
+ if match is None:
101
+ continue
102
+ lines = []
103
+ for e in parse_index(text):
104
+ if e["area"] != match:
105
+ continue
106
+ line = e["line"]
107
+ if lib.scope == "personal" and e["script"] in project_scripts:
108
+ line += " (shadowed)"
109
+ lines.append(line)
110
+ sections.append(f"## {match} ({lib.scope})\n" + "\n".join(lines))
111
+ if not sections:
112
+ raise ValueError(f"no area '{area}'. Areas: {', '.join(known) or '(library is empty)'}")
113
+ return "\n\n".join(sections)
114
+
115
+
116
+ def resolve(script):
117
+ """(Library, Path) for script, checked project-first then personal; same name => project wins."""
118
+ for lib in paths.libraries():
119
+ d = (lib.root / "scripts").resolve()
120
+ p = (d / script).resolve()
121
+ if (p.parent == d and p.is_file() and not p.name.startswith("test_")
122
+ and not p.name.endswith(".runner.json") and not p.name.endswith(".test.json")):
123
+ return lib, p
124
+ raise ValueError(f"not a library script: {script}")
125
+
126
+
127
+ def run(script, args, timeout=300):
128
+ if not paths.personal_root().is_dir():
129
+ raise ValueError(NOT_SET_UP)
130
+ lib, p = resolve(script)
131
+ if lib.scope == "project":
132
+ from . import trust
133
+ trust.require(lib.root)
134
+ if p.suffix.lower() in runners.WINDOWS_ONLY_EXTENSIONS:
135
+ runners.check_cmd_args(p, ["cmd"], args) # before runner_for, which refuses .bat off Windows
136
+ argv = runners.runner_for(p)
137
+ runners.check_cmd_args(p, argv, args)
138
+ cmd = [*argv, str(p), *args]
139
+ try:
140
+ return runners.run_process(cmd, cwd=Path(os.getcwd()), env=None, timeout=timeout)
141
+ except FileNotFoundError as e:
142
+ raise ValueError(f"failed to launch '{argv[0]}' for {p.name}: {e}")
143
+
144
+
145
+ def save(name, code, test, area, usage, *, scope="personal", new_area=False, runner=None,
146
+ test_name=None, test_runner=None, replace=False, push=None):
147
+ """Compatibility wrapper; new callers should use store.save.
148
+
149
+ Returns NOT_SET_UP for any scope when the personal library folder is missing, matching
150
+ mm_list/mm_run/mm_save's MCP behaviour (Task 6 review, finding 4): a project-scope save
151
+ still needs the personal folder to exist, since that's what marks first-run setup done.
152
+ """
153
+ from .store import save as store_save
154
+ if not paths.personal_root().is_dir():
155
+ return False, NOT_SET_UP
156
+ return store_save(name, code, test, area, usage, scope=scope, new_area=new_area,
157
+ runner=runner, test_name=test_name, test_runner=test_runner,
158
+ replace=replace, push=push)