mops is a small CLI for letting coding agents work with JetBrains MPS projects. This checkout is a Gradle-rooted
Kotlin prototype with two application subprojects: cli/ and daemon/.
mops --help
mops wrapper
mops --mps-home /path/to/mps daemon ping
mops --mps-home /path/to/mps edit model --file edit-batch.json
mops daemon status
mops daemon stopThe CLI starts or reuses a per-project daemon process for most commands.
Global options (--mps-home, --java-home, --daemon-home, --project-root, and --refs-as-urls) can appear before,
between, or after command names. Command-specific options belong after their command name.
Pass --project-root PATH to select an MPS project explicitly. The path can be absolute or relative to the directory
where mops was started, for example mops --project-root ../my-project daemon status.
Pass --refs-as-urls to render node references in text output as MPS URLs, for example
mops --refs-as-urls find instances <concept>. JSON output is unchanged.
If you do not know where MPS and Java are installed, run mops wrapper [PATH] from the project checkout. PATH is the
starting point of discovery, defaulting to --project-root when supplied, or the working directory. wrapper uses the
nearest Gradle wrapper to inspect mpsDefaults of the com.specificlanguages.mps Gradle plugin or RunAntScript tasks
of the de.itemis.mps.gradle.common plugin. By default it writes a wrapper for every MPS project in every Gradle
project in the build. Each mopsw on macOS and Linux, or mopsw.cmd on Windows, is written directly in the Gradle
project's build directory when it has one detected MPS project. With multiple MPS projects, wrappers are written under
<build-directory>/mops/<MPS-project-name>/. Pass mops wrapper --project-root PATH to write only that MPS project's
wrapper; --project-root can also precede wrapper. Reported paths are relative to the working directory. When one
wrapper is selected, --output PATH selects its exact file; a relative path is resolved from the directory where mops
was started. The POSIX wrappers are executable. Each wrapper supplies the shared discovered MPS and Java homes and its
MPS project directory. MPS projects come from configured mpsBuilds entries and .mps project markers in the Gradle
build. Each wrapper invokes the full path of the mops installation that generated it, so mops need not be in PATH. Run
the generated wrapper in place of mops; arguments passed to it are forwarded to mops. No MPS home or daemon is
required in the CLI invocation that writes the wrapper; Java must be available to run Gradle.
Discovery queries configured providers, which can download and extract distributions. It does not execute preparation or
language build task actions. It inspects all Gradle projects in the build and uses a discovered mpsDefaults extension
or conventional RunAntScript task for the shared runtime paths. Provider resolution failures stop discovery and retain
Gradle’s dependency diagnostics; check the configured runtime versions and repositories. Partial discoveries show
available paths without writing a wrapper and exit with status 1; when both paths are known, the command writes the
wrappers and exits with status 0. Missing or unusable runtime directories are reported as warnings, so wrappers can be
created before the project's documented preparation steps. If a pinned MPS prerelease is no longer available, update the
project’s MPS prerelease version to the newest available version; prerelease artifacts are periodically deleted.
Task-action overrides, included builds, and custom Ant property loading are outside this heuristic; Specific Languages
values are project defaults.
Run mops skill path to print the absolute path to the bundled mops/SKILL.md. Ask your coding agent to load that
file, or symlink its containing directory into your agent's skills directory to follow mops upgrades. This command works
without an MPS project or daemon.
Use mops skill path daemon for daemon coordination, restart, and recovery guidance.
The mops skill covers command selection, model navigation, editing, builds, checks, tests, and Code Mode. Its Groovy examples are executed directly from the skill file by daemon unit tests.
For short task-oriented examples, run mops examples or read the
bundled topic index. Start with mops examples editing to choose an editing
approach, then read a focused topic such as mops examples editing.java. mops examples all prints the full
collection. Examples work offline without a daemon.
Commands that operate on MPS data use a verb-first shape: mops <verb> <object>. Operational commands use a concrete
subsystem or artifact name, such as daemon, code, and wrapper.
mops --mps-home <path> daemon pingStarts or reuses the persistent daemon for the current MPS project, exchanges one ping request over a loopback socket,
and prints the structured response. The command walks upward from the current directory until it finds a .mps
directory.
mops --mps-home <path> edit model [--file PATH] [--constraints advisory|best-effort|strict]Applies a JSON batch of edit operations through the daemon. Run mops explain edit for the operation reference. An
operation that names a concept resolves it the same way find instances does: a dropped .structure. infix is
forgiven, and a name that does not resolve fails the batch (changing nothing) with the same diagnosis — unknown
language, unloaded language, or a "did you mean" among a loaded language's concepts.
--constraints selects how the batch treats MPS constraints and concepts whose language did not load (default
best-effort):
advisory- evaluate constraints and report violations, but apply and save the batch anyway.best-effort- a violation blocks the batch (nothing is saved); a concept that cannot be checked because its language did not load is skipped and reported as a warning.strict- likebest-effortfor violations, but a concept that cannot be checked aborts the batch.
mops --mps-home <path> get node [--ancestry] <node-reference>
mops --mps-home <path> get node [--ancestry] <model-target> <node-id>Exports one resolved node as a JSON tree through the daemon, addressed by a serialized node reference or by a model
target plus node ID. An unresolved target fails with NODE_NOT_FOUND. The exported object carries the node's concept,
id, properties, references, and child subtree, plus a parent object describing its immediate containing node: the
containment role by which the node sits in it, a type of root or node, and the parent's name, concept, and
reference. A root node has no parent. --ancestry nests parent recursively up to the root node instead of carrying
only the immediate parent.
mops --mps-home <path> list [--depth N] [--limit N] [--summary] [--role ROLE] [--full-concept] [--json] [TARGET_SEGMENT...]
mops --mps-home <path> ls [--depth N] [--limit N] [--summary] [--role ROLE] [--full-concept] [--json] [TARGET_SEGMENT...]Lists an MPS navigation target as a bounded tree. Space-separated target segments address a module, model, or node; omit
them for the project root, or pass / for the repository root. --depth bounds the descendant depth from 0 to 8
(default 1). Text output is one indented tab-separated row per entry, tagged by kind (project, repository, module,
model, root, or node); --json prints the semantic tree.
--limit N caps how many children each level shows (default 50; 0 is unlimited). A level wider than the cap is
truncated and ends in an indented # truncated: showing <shown> of <total> children; pass --limit 0 for all line, so
the omission is explicit and the line is told apart from a row by its #. --summary replaces the target's children
with grouped counts — per role (with the dominant concepts) for a node, per concept for a model's roots, per model
for a module, per kind for the project or repository — and cannot be combined with --depth. --role ROLE lists only
the target node's children in one containment role and is valid only for a node target.
Text output shows short concept names; --full-concept restores the fully qualified names. JSON output keeps qualified
concept names regardless.
mops --mps-home <path> render node [--allow-reflective] <node-reference>
mops --mps-home <path> render node [--allow-reflective] <model-target> <node-id>Renders one resolved node as the plain text of its default editor — the way it would appear in the MPS editor — and
prints it with the editor's line breaks and indentation, trimming trailing whitespace from each line. Addressed the same
way as get node: a serialized node reference, or a model target plus node ID. An unresolved target fails with
NODE_NOT_FOUND. Any node is renderable, not only root nodes; the output is a quick overview for reading, not a
round-trippable serialization.
If any concept in the node's subtree does not resolve — its language is not loaded — the command fails with
LANGUAGE_NOT_LOADED. It diagnoses each unloaded language through the same machinery as find instances (naming the
root cause: not built, absent, or a broken dependency) and points at mops make module <language> and
mops diagnose module <language>. Pass --allow-reflective to render anyway. A concept whose language is loaded but
that simply defines no editor is not an error: MPS renders it with its generic reflective editor (concept aliases and
roles rather than the language's own notation), which the command prints as-is.
mops --mps-home <path> find instances [--exact] [--limit N] [--full-concept] [--refs-only] [--json] <concept>Searches editable project sources for nodes that are instances of an MPS concept, including subconcepts and MPS
interface matches by default. The concept may be named three ways: its qualified name
(<language>.structure.<ConceptName>), the same name with .structure. dropped, or a bare short name that is unique
across loaded languages. --exact restricts results to nodes whose direct concept is the queried one. An existing
concept with no matches succeeds with no rows.
A concept that does not resolve fails with CONCEPT_NOT_FOUND, and the error explains which of the causes applies: the
name is not well formed; the owning language is unknown to the project; the owning language is present but not loaded
(in which case it reports that language's load diagnosis and points at diagnose module, since only loaded languages
contribute concepts to name lookup); or the language is loaded but has no such concept (in which case it suggests
similarly named concepts from that language). A bare short name is resolved by counting matches across loaded languages:
one that names concepts in several languages fails with AMBIGUOUS_TARGET and lists the qualified candidates, and one
offered while a project language is unbuilt fails with LANGUAGE_NOT_LOADED rather than guessing, since the unbuilt
language could define it too.
Text output is tab-separated rows of root or node, the node name (or <unnamed>), the node's actual concept, and
its serialized node reference; a non-root node appends its immediate parent as trailing parent, parent name (or
<unnamed>), parent concept, and parent reference columns. Concept columns show short names by default;
--full-concept restores the fully qualified names, while JSON output keeps them regardless. --refs-only prints just
one serialized node reference per line — nothing else — so results pipe straight into mops get node; it is mutually
exclusive with --json and reports any truncation on stderr. --json prints an object with limit, truncated, and a
nodes array whose non-root entries carry a nested parent summary.
mops --mps-home <path> find usages [--limit N] [--full-concept] [--refs-only] [--json] <node-reference>
mops --mps-home <path> find usages [--limit N] [--full-concept] [--refs-only] [--json] <model-target> <node-id>Searches editable project sources for references to one resolved target node, addressed the same way as get node.
An unresolved target fails with NODE_NOT_FOUND. Text output is tab-separated rows of usage, the reference role, and
the owning node's name (or <unnamed>), concept, and reference; the owner is typed root or node by its position in
its model. A non-root owner appends its immediate parent as trailing parent, parent name (or <unnamed>), parent
concept, and parent reference columns. --json prints an object with limit, truncated, and a usages array whose
entries carry the reference role and a nested owner node summary, itself carrying a nested parent summary for a
non-root owner. Concept columns follow the same short-name default, --full-concept, and JSON-keeps-qualified rules as
find instances; --refs-only prints one serialized reference per referencing node. Both find modes default to
--limit 100, treat --limit 0 as unlimited, reject negative limits, and append a
# truncated: showing <shown>; pass --limit 0 for all line (in text) or set truncated (in JSON) only when more
matches exist than were returned. Lines starting with # are metadata, never results, so grep -v '^#' leaves only
rows.
mops --mps-home <path> find root-by-name [--limit N] [--full-concept] [--refs-only] [--json] <pattern> [in <scope-segment>...]Finds root nodes by name using MPS Go-to-Node pattern matching. The pattern supports camel-hump and * wildcards (see
mops explain name-pattern). By default it searches editable project sources; append in <scope-segments> to
search a module, model, or the whole repository (in /), where only root-bearing scopes are valid (see
mops explain scope). It defaults to --limit 100, treats --limit 0 as unlimited, and rejects negative limits. Text
output is tab-separated rows in the same shape as find instances, including the short-concept default with
--full-concept and the --refs-only piping mode; --json prints the structured response.
mops --mps-home <path> diagnose project [--all] [--json]Reports the load state of the project's languages and every other project module that has a Java facet, so a
CONCEPT_NOT_FOUND from find instances can be traced to its cause: a concept resolves by name only if its owning
language's runtime is loaded. Text output starts with a modules\t<loaded>/<total> loaded\t<failed> failed summary,
then one tab-separated row per failed module — its name, kind, and a reason code — followed, when the reason is
BROKEN_DEPENDENCIES, by indented rows naming the root-cause modules to fix. It ends with a note line: modules
without a Java facet are not listed and can be inspected individually with diagnose module. --all also lists the
modules that loaded; --json prints the full structured diagnosis.
mops --mps-home <path> diagnose module [--json] <module>Diagnoses one module, addressed by module name or serialized module reference, including modules not shown by
diagnose project (those without a Java facet, or absent from the repository). Text output is a header row (module,
kind, present=<bool>, loaded=<bool>) followed by the module's load-problem tree, indented by depth so a dependency
chain reads from the module down to its root causes; --json prints the structured diagnosis.
Both commands classify an unloaded module with a reason code: ABSENT (not in the repository), NOT_A_MODULE (resolves
to something that does not load classes), NO_JAVA_FACET (no Java facet — a defect for a language, informational for
another module), CLASSES_DISABLED (the Java facet is configured not to load classes), NOT_BUILT (classes not
generated/compiled yet), BROKEN_DEPENDENCIES (blocked by depended-on modules, reported recursively), and
RUNTIME_LOAD_FAILED (everything present but the runtime still did not register — usually a class-link or version error
in the daemon log).
mops --mps-home <path> make module [--json] <module>...
mops --mps-home <path> make project [--json]Runs the MPS make (generation and compilation) through the daemon. make module makes the named modules (by module name
or serialized module reference) and their transitive dependency closure, so an un-made dependency is made too;
make project makes every generatable module in the project. Both exit non-zero when the make reports errors; --json
prints the make result as JSON.
mops test # all project tests, with an incremental build
mops test my.module .tests MyCase # navigation target: module, model, test case
mops test '<test-node-reference>' --no-build --json
mops test --timeout 60Tests run in a separate MPS process using the selected distribution's testing framework. Selection accepts a project
(omit the target), module, model, or root test case. Individual test methods are unsupported. The default build includes
the owning modules and required dependencies. --no-build requires existing compiled classes. The overall timeout
defaults to 900 seconds; --timeout 0 disables it. Ctrl-C cancels the worker. Standalone cancellation and timeouts
leave the daemon usable. Execution uses the distribution's Ant launchtests machinery in IDEA test mode, including its
test session, failure detection, and XML reports. The test JVM's classpath is constructed independently of the normal
daemon's boot classpath. Plugins are discovered automatically. On MPS 2025.1 only, Images
(com.intellij.platform.images) is disabled to avoid its SVG parser classloader defect; tests requiring Images are
unsupported on that version. Model and test-case filtering occurs after module-wide class discovery, so broken
unselected classes can also fail a run, particularly with --no-build. A matching JBR with javac is required to
compile the version-specific adapter.
Every run saves report.json under the daemon workspace's test-runs/<run-id>/ directory and updates it as results
arrive. Text output prints a summary and the report path; --json prints the report. Tests, containers, skips,
assumption aborts, failures, and source node references are retained. No discovered tests, preparation failures, test or
container failures, and incomplete runs exit 1. Skips and assumption aborts alone do not fail a run. Worker output is
saved next to the report in worker.log (preparation) and ant.log (test execution); XML reports are in
xml-reports/.
Code Mode provides the same operation outside model-access blocks:
def model = project.read { mops.lookup.requireModel('my.module.tests@tests') }
def report = mops.testing.run(model, [build: true, timeout: 60])
return reportThe selection may also be the native project, module, or test node. Options default to build: true, timeout: 900. A
failed report does not change the Code Mode exit status. The enclosing Code Mode deadline still terminates its daemon;
the test worker detects that termination and saves an incomplete report. Project operations are serialized during a run,
and changes are saved before launch. Tests use the existing checkout and may change its files.
mops explain [--schema] [PATH]Prints reference pages for the mops notations, the textual formats mops exchanges with agents. It is pure and offline:
it never starts a daemon, resolves a project root, or requires --mps-home. With no argument it lists the topics; a
dot- path such as edit or edit.copyAsChild prints that page, and an unknown path exits non-zero with sibling
suggestions. --schema prints the generated JSON Schema for the edit-batch notation (only with the edit topic).
mops daemon status [--all]
mops daemon stop [--all]Inspect or stop known per-project daemon processes. Without --all, the command infers the current project from the
working directory. With --all, it reads every known daemon record. Status sends an authenticated ping with a
two-second connection/read timeout and reports running or unreachable. It does not start daemons or remove
unreachable records. A daemon can be unreachable because it is stopped, unresponsive, or unable to authenticate the
request. Pings remain responsive during normal long operations.
Stop sends a stop request to each selected daemon and waits for its process to exit, force-killing it if necessary. A
stopped daemon's record is removed and reported as stopped. If the stop request gets no response, the record is
removed as stale and reported as removed stale daemon record ...; process not signalled; the recorded process is not
killed. If the daemon acknowledges the stop but its process survives the force-kill, the record is kept so the next
daemon stop retries it, the PID is reported on stderr, and the command exits non-zero.
Daemons detach from the launching command's session on Unix and from its console on Windows. Their stdout and stderr are written to the daemon log, and stdin is closed. Filesystem and network sandbox restrictions still apply.
The CLI allows 300 seconds (5 minutes) for initial daemon startup, including opening the MPS project. For larger
projects, set MOPS_DAEMON_STARTUP_TIMEOUT_SECONDS to a positive whole number of seconds, for example:
export MOPS_DAEMON_STARTUP_TIMEOUT_SECONDS=600This setting controls the wait for the daemon's readiness record. It does not change command execution timeouts. If startup times out, the CLI terminates the daemon and reports the timeout and daemon log path.
Daemon records, logs, working files, and isolated IDEA config and system directories live outside the MPS project. By
default the CLI stores them under $XDG_CACHE_HOME/mops/daemon, falling back to ~/.cache/mops/daemon when
XDG_CACHE_HOME is unset; pass --daemon-home <path> to use another directory. Each project gets a stable hashed
subdirectory under projects/, including:
daemon.json- atomic daemon record with port, token, PID, daemon version, project path, MPS home, Java home, workspace path, and startup timelogs/daemon.log- daemon startup and runtime log for the current prototypedaemon/configanddaemon/system- isolated IDEA directories passed to the daemon JVM
Daemon commands use loopback socket IPC with a per-daemon token. Independent list, get-node, find, diagnostic, and Code Mode help requests can run in reader batches. Edits, creation, builds, tests, checks, rendering, and Code Mode execution have exclusive admission; ping and stop remain responsive independently of project work. Stale daemon records are removed when the recorded process or socket is no longer reachable.
See the developer documentation for building, running, and testing mops.
Releases are published from explicit release commits on main:
- Change
versioningradle.propertiesfrom the development snapshot to the exact release version and merge the release commit. - Run the Release mops workflow from
main, entering the version without thevprefix. - After publication, change
gradle.propertiesto the next development-SNAPSHOTversion.
The workflow verifies the version, runs all checks, smoke-tests the ZIP distribution, generates its SHA-256 checksum,
and publishes both files in a GitHub Release with notes generated from the merged pull requests. It creates the
v<version> tag at the tested release commit; release tags must not be created manually.
The distribution includes a pinned Groovy core runtime. The daemon uses MPS's Groovy runtime when present and the bundled core otherwise; no additional JAR or classpath override is required. Stop existing daemons after updating mops.