@moonarc/mcp 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/bin/moonarc.js +80 -0
- package/catalog.json +11809 -0
- package/package.json +48 -0
- package/src/audit.js +365 -0
- package/src/catalog.js +186 -0
- package/src/compose.js +171 -0
- package/src/install.js +29 -0
- package/src/license.js +47 -0
- package/src/measure.js +269 -0
- package/src/server.js +196 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 azriPtr
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @moonarc/mcp
|
|
2
|
+
|
|
3
|
+
MCP server for [Moonarc](https://moonarc.dev). Local, stdio, no account.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
claude mcp add moonarc -- npx -y @moonarc/mcp
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Cursor, VS Code and Codex take the same command in their own config files:
|
|
10
|
+
[moonarc.dev/mcp](https://moonarc.dev/mcp/) has each one.
|
|
11
|
+
|
|
12
|
+
Tools: `search_motion(intent, max_js_bytes?, tier?, limit?)`, `get_component(name)`,
|
|
13
|
+
`get_install_command(names, method?)`, `get_usage()`. The catalogue is bundled
|
|
14
|
+
at publish time. `MOONARC_CATALOG_URL` reads a catalogue of the same shape from
|
|
15
|
+
a URL instead, a mirror or a test's; when that URL fails, takes over 5 s or
|
|
16
|
+
answers another shape, the bundled copy answers.
|
|
17
|
+
|
|
18
|
+
## Pro
|
|
19
|
+
|
|
20
|
+
With a Pro key in `MOONARC_KEY`, three more tools appear: `audit_motion(paths, format?)`,
|
|
21
|
+
`measure_budget(dist, ceiling?, base?, format?)` and `compose_section(intent)`.
|
|
22
|
+
The server asks Polar whether the key is valid and keeps a definite answer for
|
|
23
|
+
the life of the process. Without a key and without `MOONARC_CATALOG_URL`, it
|
|
24
|
+
makes no network request.
|
|
25
|
+
|
|
26
|
+
The client starts the server, and most clients pass it only a few of your
|
|
27
|
+
environment variables, so put the key in the client's config:
|
|
28
|
+
|
|
29
|
+
- Claude Code: `claude mcp add --env MOONARC_KEY=<your key> --transport stdio moonarc -- npx -y @moonarc/mcp`
|
|
30
|
+
(keep an option between `--env` and the name, or the CLI reads the name as another variable)
|
|
31
|
+
- Cursor, in `.cursor/mcp.json`: `"env": { "MOONARC_KEY": "${env:MOONARC_KEY}" }` beside `command`
|
|
32
|
+
- VS Code, in `.vscode/mcp.json`: an input `{ "type": "promptString", "id": "moonarc-key", "password": true }`
|
|
33
|
+
and `"env": { "MOONARC_KEY": "${input:moonarc-key}" }`
|
|
34
|
+
- Codex, in `~/.codex/config.toml`: `env_vars = ["MOONARC_KEY"]` under `[mcp_servers.moonarc]`
|
|
35
|
+
|
|
36
|
+
`get_usage` tells you which plan the server validated.
|
|
37
|
+
|
|
38
|
+
The same package runs the audit and the byte meter as commands, for CI:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npx -y @moonarc/mcp audit src
|
|
42
|
+
npx -y @moonarc/mcp measure dist --ceiling 30000
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`audit` exits 1 when it finds an error. `measure` reads the folder `astro build`
|
|
46
|
+
wrote (a server build's `dist/client` is found by itself), takes `--ceiling` in
|
|
47
|
+
raw bytes per page and `--base` when the site sets one, and exits 1 when a page
|
|
48
|
+
is over the ceiling. Both exit 2 on a usage mistake, a refused key or a folder
|
|
49
|
+
that is not a build.
|
package/bin/moonarc.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Both bins run this file. npx runs a package that has several bins only when they all name the same file (or one of
|
|
3
|
+
// them is named after the package, here `mcp`), so one file is what lets `npx -y @moonarc/mcp` start the server for an
|
|
4
|
+
// agent and `npx @moonarc/mcp audit src` run the CI command.
|
|
5
|
+
// no command, stdin not a terminal the MCP server on stdio: an MCP client started it
|
|
6
|
+
// audit [paths…] [--format json] exit 1 on errors
|
|
7
|
+
// measure [dist] [--ceiling BYTES] [--base PATH] [--format json]
|
|
8
|
+
// Exit 2 is everything that is not a result: a usage mistake, a key the licence check refuses, a folder that is not
|
|
9
|
+
// a build. Arguments are checked before the key, so a typo never waits on the network.
|
|
10
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
11
|
+
const usage = `usage: moonarc audit [paths…] [--format text|json]
|
|
12
|
+
moonarc measure [dist] [--ceiling BYTES] [--base PATH] [--format text|json]
|
|
13
|
+
exit 0 when clean, 1 on an error finding or a page over the ceiling, 2 on a usage, key or build problem.
|
|
14
|
+
An MCP client starts the server with: npx -y @moonarc/mcp`;
|
|
15
|
+
|
|
16
|
+
if (!cmd && !process.stdin.isTTY) {
|
|
17
|
+
const { start } = await import('../src/server.js');
|
|
18
|
+
start().catch((err) => {
|
|
19
|
+
console.error(err);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
});
|
|
22
|
+
} else if (cmd !== 'audit' && cmd !== 'measure') {
|
|
23
|
+
console.log(usage);
|
|
24
|
+
process.exit(cmd && !['help', '--help', '-h'].includes(cmd) ? 2 : 0);
|
|
25
|
+
} else {
|
|
26
|
+
const stop = (message) => {
|
|
27
|
+
console.error(`moonarc: ${message}\n${usage}`);
|
|
28
|
+
process.exit(2);
|
|
29
|
+
};
|
|
30
|
+
const FLAGS = { audit: ['--format'], measure: ['--format', '--ceiling', '--base'] }[cmd];
|
|
31
|
+
const flags = {};
|
|
32
|
+
const args = [];
|
|
33
|
+
for (let i = 0; i < rest.length; i++) {
|
|
34
|
+
const a = rest[i];
|
|
35
|
+
if (a === '--help' || a === '-h') {
|
|
36
|
+
console.log(usage);
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
if (!a.startsWith('-')) {
|
|
40
|
+
args.push(a);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const [name, inline] = a.split(/=(.*)/s);
|
|
44
|
+
if (!FLAGS.includes(name)) stop(`${cmd} has no option ${name}`);
|
|
45
|
+
const value = inline ?? rest[++i];
|
|
46
|
+
if (value === undefined || value === '' || value.startsWith('--')) stop(`${name} needs a value`);
|
|
47
|
+
flags[name.slice(2)] = value;
|
|
48
|
+
}
|
|
49
|
+
if (flags.format && !['text', 'json'].includes(flags.format)) stop(`--format is text or json (you gave "${flags.format}")`);
|
|
50
|
+
// --ceiling 30kB used to become NaN, and no page is over NaN: the check passed whatever the build weighed
|
|
51
|
+
if (flags.ceiling !== undefined && !(/^\d+$/.test(flags.ceiling) && Number(flags.ceiling) > 0)) stop(`--ceiling takes a whole number of raw bytes, such as 30000 (you gave "${flags.ceiling}")`);
|
|
52
|
+
if (cmd === 'measure' && args.length > 1) stop(`measure takes one folder (you gave ${args.length}: ${args.join(', ')})`);
|
|
53
|
+
const json = flags.format === 'json';
|
|
54
|
+
|
|
55
|
+
const { licenseStatus } = await import('../src/license.js');
|
|
56
|
+
const { loadCatalog, libraryChunks } = await import('../src/catalog.js');
|
|
57
|
+
const st = await licenseStatus();
|
|
58
|
+
if (!st.ok) {
|
|
59
|
+
const { site } = await loadCatalog();
|
|
60
|
+
console.error(`moonarc: ${cmd} is a Pro tool: ${st.reason}. ${site}/pro/`);
|
|
61
|
+
process.exit(2);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
if (cmd === 'audit') {
|
|
66
|
+
const { audit, formatAudit } = await import('../src/audit.js');
|
|
67
|
+
const r = audit(args.length ? args : ['src']);
|
|
68
|
+
console.log(json ? JSON.stringify(r, null, 2) : formatAudit(r));
|
|
69
|
+
process.exit(r.findings.some((f) => f.severity === 'error') ? 1 : 0);
|
|
70
|
+
} else {
|
|
71
|
+
const { formatMeasure, measureDist } = await import('../src/measure.js');
|
|
72
|
+
const r = measureDist(args[0] ?? 'dist', { ceiling: flags.ceiling ? Number(flags.ceiling) : undefined, base: flags.base, library: libraryChunks(await loadCatalog()) });
|
|
73
|
+
console.log(json ? JSON.stringify({ ...r, routes: r.routes.map(({ assets, ...x }) => x) }, null, 2) : formatMeasure(r));
|
|
74
|
+
process.exit(r.over.length ? 1 : 0);
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error(`moonarc: ${err.message}`);
|
|
78
|
+
process.exit(2);
|
|
79
|
+
}
|
|
80
|
+
}
|