ml-skills-mcp
by MLMCPS
README.md
# ml-skills
**Architecture standards you can actually enforce** — served as an MCP server, an HTTP API, a CLI
and a library, so the same rules reach your coding agents, your website, your pipeline and your
editor without being written down four times.
```bash
npx @mlmcps/ml-skills check . # a repo, with file:line findings
npx -p @mlmcps/ml-skills ml-skills-mcp # MCP over stdio, for agents
npx -p @mlmcps/ml-skills ml-skills-http # HTTP API + MCP Streamable HTTP + browser UI
```
---
## The idea
Most "AI coding standards" projects ship a pile of markdown and hope. The problem is that prose is
unenforceable, and an agent that reads a standard is under no obligation to follow it. So this
package separates two things that usually get conflated:
| | What it is | Who owns it |
|---|---|---|
| **Rules** | The checks. *"Pick one pagination style and use it everywhere."* | Everyone. Shipped here. |
| **Decisions** | The values those checks compare against. *"Ours is cursor pagination with `cursor`/`limit`."* | You. Overridable, and the only part that is anyone's IP. |
You cannot buy the second column — a standard's value is precisely that it is *your* decision,
consistently applied. What you can take off the shelf is the machinery: the rule catalogue, the
checkers, and three transports to reach every tool you use. That is this package.
### Nothing here can fail your build until you say so
Every standard ships marked `proposed`. A proposed standard reports at `warn` and **cannot** exit
non-zero, whatever its severity map says. You ratify it in `.mlskills.json`, and only then can it
break a pipeline.
That is deliberate. A gate that fails on day one gets disabled on day two, and a rule nobody agreed
to is a preference wearing a uniform. Every report says out loud which standards were incapable of
failing, because *"no errors"* means something very different when nothing was allowed to be one.
---
## The eleven standards
| Standard | Governs | Rules |
|---|---|---|
| `openapi-contract` | HTTP API shape: versioning, errors, pagination, headers, leaks | 22 |
| `entity-relationships` | SQL keys/money/time/FKs **+ Mongoose schema design** | 36 |
| `backend-patterns` | Layering, secrets, logging, timeouts, input validation | 25 |
| `infra-patterns` | Images, probes, limits, pod security, CI pinning | 21 |
| `ui-patterns` | Async states, tokens, data layer, accessibility | 19 |
| `security-patterns` | Injection, authz, crypto, cookies, traversal, headers | 22 |
| `testing-patterns` | Whether a green suite means anything | 14 |
| `events-messaging` | Versioning, idempotency, the outbox, dead letters | 10 |
| `observability` | Structured logs, correlation, traces, metrics | 10 |
| `resilience-patterns` | Backoff, jitter, breakers, graceful shutdown | 10 |
| `config-secrets` | Twelve-factor config, startup validation, secret hygiene | 10 |
**199 rules.** Read one in full: `npx @mlmcps/ml-skills show security-patterns`.
A few that earn their keep:
- **SEC021** flags `findById(req.params.id)` with no ownership check nearby. Broken object-level
authorisation is the most exploited API flaw there is, and it looks exactly like working code.
- **OAS009** resolves `$ref` transitively, so a `passwordHash` two schemas below a response root is
still reported — the shape credential leaks actually have.
- **EVT003** flags a publish inside a transaction with no outbox: the dual-write, where the
transaction rolls back, the event does not, and the estate now believes something that never
happened.
- **RES010** flags `catch { return [] }`. The dependency is down, the page renders "no results",
nothing alerts, and revenue drops for a week.
- **TST001** flags a committed `.only`, which silently stops the rest of the suite from running
while the build stays green.
## Adapters — reuse what already exists
Spectral, Semgrep, hadolint, ESLint and Trivy encode far more person-years of rule development
than this package ever will, and several match on a real AST rather than on text. Reimplementing
them would be worse and slower. So ml-skills runs them and normalises what they say:
```bash
ml-skills adapters # what is installed
ml-skills check . --with semgrep # built-in rules plus one adapter
ml-skills check . --with-all # plus everything installed
```
| Adapter | Standard it reports under | Covers |
|---|---|---|
| `spectral` | `openapi-contract` | OpenAPI/AsyncAPI rulesets |
| `hadolint` | `infra-patterns` | Dockerfile |
| `eslint` | `backend-patterns` | **your project's own** ESLint config — this adapter adds no rules |
| `semgrep` | `security-patterns` | semantic patterns, registry rulesets |
| `trivy` | `security-patterns` | dependency CVEs, committed secrets, IaC misconfiguration |
| `squawk` | `entity-relationships` | **Postgres migration safety** — parses SQL, reasons about lock levels |
| `sqlfluff` | `entity-relationships` | dialect-aware SQL linting |
| `spotbugs` | `backend-patterns` | Java bytecode analysis (+ Find Security Bugs) — **reads `target/*.xml`** |
| `pmd` | `backend-patterns` | Java source rules — reads the build's report |
| `checkstyle` | `backend-patterns` | Java conventions — reads the build's report |
**Java adapters read the report your build already produced** rather than running a JVM. PMD,
SpotBugs and Checkstyle are Maven and Gradle plugins, not standalone CLIs — invoking a second copy
would be slower, need its own configuration, and could disagree with the one that gated the build.
Their applicability is the report's existence, not our file walk: the build may have analysed
sources we never saw.
ArchUnit has no adapter and needs none. It is a test library, so layering rules run in your
existing suite and fail the build with no extra gate.
Findings merge into one list, sorted together, each tagged with its source:
```
Dockerfile
1 error INF001 base image "node:latest" uses the :latest tag
2 warn hadolint/DL3009 via hadolint Delete the apt-get lists after installing something
2 warn INF007 apt install without pinned package versions
```
**External findings go through the same ratification gate.** An installed tool cannot grant itself
the authority to fail a build on a standard nobody has signed off. Severity precedence is: your
config override, then the tool's own severity, then the ratification demotion.
```jsonc
{
"adapters": {
"semgrep": { "required": true, "config": "p/owasp-top-ten" },
"hadolint": { "severity": { "DL3059": "off" } },
"trivy": { "scanners": "vuln,secret" }
}
}
```
## Stack presets
```bash
ml-skills preset # list
ml-skills preset mern # write .mlskills.json
ml-skills preset java-spring
```
| Preset | Adapters | Rules deferred |
|---|---|---|
| `mern` | 5 | 65 |
| `java-spring` | 9 | 70 |
A preset answers the question that matters once you accept the ecosystem is stronger than any one
package: **given ~3,000 rules across twenty tools, which of ours are still worth running?**
Two rules govern them, both enforced by tests:
- **Never silence a rule without naming the tool that supersedes it.** Every block carries a
`$why`. An unexplained `"off"` is indistinguishable from someone quietly disabling an
inconvenient rule, so it is not allowed to exist.
- **A preset never ratifies anything.** Every skill stays `proposed`. Deciding a standard may break
a build is a human call, and a config file taking it on your behalf is how governance gets
disabled the week after it lands.
Applying a preset over an existing config **keeps your values** and reports which it kept — a
preset is a starting point, not an overwrite.
### What each preset keeps
`mern` defers UI, testing and most security rules to `eslint-plugin-react`/`-hooks`/`-jsx-a11y`
(171 rules), `eslint-plugin-jest`/`-testing-library` (100) and Semgrep — then keeps the **Mongoose
schema rules**, because as of writing nothing in the JavaScript ecosystem checks Mongo *schema
design*. `eslint-plugin-mongodb` is 17 rules about query syntax; `eslint-plugin-mongoose` does not
exist. That is the load-bearing part of the preset.
`java-spring` defers bug-hunting to PMD, SpotBugs and Checkstyle, and **migration safety to
squawk** — which parses Postgres and reasons about lock levels (*"requires ACCESS EXCLUSIVE;
blocking: reads, writes"*), an analysis no pattern over text can reach. It keeps the contract and
design decisions: layering, entities leaving the API layer, timeouts, PII, transaction placement,
key type, cross-service foreign keys.
Both keep `events-messaging`, `observability` and `resilience-patterns` in full, because nothing
in either ecosystem checks the outbox pattern, correlation propagation, or backoff-with-jitter.
`resilience4j` is a *library* — using it does not mean it was configured correctly.
### A tool that did not run is never a clean result
This is the rule the whole adapter layer is built around. A missing scanner reported as "no
findings" is worse than no scanner, because the first one gets believed.
- Not installed → status `missing`, with an install hint.
- Crashed or timed out → status `failed`, with the reason and the exact command to reproduce.
- Output unparseable → status `failed`, **not** zero findings.
- Marked `required` and absent → an **error-severity finding**, so the gate fails.
- Either way, the summary carries `adaptersNotRun` and the report says so at the top level.
`ml-skills` own rules do not inspect a lockfile at all. A clean run says nothing about the CVEs in
your dependencies — only the Trivy adapter covers those, and `adapter_status` will tell an agent so
before it promises a security verdict.
### Overlap is expected
hadolint's `DL3025` and this package's `INF012` both flag shell-form `CMD`. That is fine — two
tools agreeing is not a bug — but if the duplication is noisy, silence one side in config rather
than dropping the adapter.
### Not over HTTP
Adapters run from the CLI and the stdio MCP server only. `POST /v1/check` never shells out: a
service that executed local binaries on request content would be a remote code execution surface,
and the HTTP face already refuses to accept a path for the same reason.
## Behavioural evals
A standard is a behavioural intervention, not a document. `evals/` tests whether it actually
changes what gets written:
```bash
ml-skills eval # all scenarios
ml-skills eval EV001 # one
```
Each scenario records the **verbatim excuse** an agent makes when the standard is absent, names the
rule that counters it, and pairs it with the code that excuse produces plus the corrected version:
```
✓ EV001 events-messaging Publishing an event inside the transaction that may roll back
✓ counter rules exist
✓ fires on the rationalized code
✓ silent on the corrected code
✓ counter is documented in the skill
⏳ "I'll commit first and then publish, so the event only fires on success."
countered by EVT003 · behavioural check pending
```
The method is borrowed, with thanks, from
[a-pavithraa/springboot-skills-marketplace](https://github.com/a-pavithraa/springboot-skills-marketplace)
(MIT). No code was copied — see [NOTICE.md](NOTICE.md) for the full provenance and the licences of
every tool the adapters invoke.
The difference is that there the counter is a *sentence*, so only a human or a model can judge
whether it landed; here it is a *rule*, so three of the four checks run in CI.
**The fourth never passes.** Whether an agent reaches for the excuse when the standard is absent
needs a model run, and is reported as `pending` — never as passing. A harness that scored itself on
the three cheap checks and let the fourth be inferred would be measuring the wrong thing while
sounding confident about it, which is the failure this package exists to prevent.
Two things the harness enforces on the scenarios themselves, because they are what make it worth
doing: the excuse must be **verbatim and plausible** (an excuse rewritten to look foolish tests
nothing), and a rule that fires on the **corrected** code is a false positive on the compliant
spelling — worse than a missing rule, because it teaches people the checker is noise.
## Workflows — what to do, in what order
Rules say what is wrong. Assets show what right looks like. Neither says **what to do first**, and
an agent handed eleven standards and ninety assets has no ordering — so it reads whatever appeared
first in the diff and reports whatever it happened to notice.
```
workflow_list # the procedures, with their "not for" clauses
workflow_get review-java # the numbered steps
```
| Workflow | Steps | For |
|---|---|---|
| `review-java` | 4 | A diff, module or PR — checker first, contract pass before correctness |
| `create-java-service` | 6 | Assess the shape, choose the structure, apply the foundations |
| `java-persistence` | 5 | Name the persistence problem, then pick the pattern |
| `upgrade-java` | 5 | Scan, then one migration at a time, each green before the next |
They own no content — they sequence the standards, references and assets that already exist. Each
is also an MCP prompt, so a client gets them as commands, and each carries a **`not-for`** clause,
because a procedure applied to a task it does not fit produces confident, irrelevant output rather
than an obvious failure.
Two tests keep them honest: steps must be numbered without gaps, and **every asset or reference a
workflow names must actually exist** — a procedure that sends an agent looking for a missing file
gets improvisation instead.
The shape is borrowed from
[a-pavithraa/springboot-skills-marketplace](https://github.com/a-pavithraa/springboot-skills-marketplace),
whose `SKILL.md` files are procedures rather than rule lists. That comparison is where this gap
became obvious — see [NOTICE.md](NOTICE.md).
## Reference documents and runnable examples
Each skill's `SKILL.md` is a router, not a dump — it carries the decisions and the rule table, and
points at a reference loaded only when a change touches that ground:
| Skill | Reference |
|---|---|
| `entity-relationships` | `mongoose-schema-design.md` — embed-vs-reference, indexes, money, the `Date.now()` trap |
| `events-messaging` | `outbox-and-delivery.md` — the dual write, at-least-once, dead letters, payload evolution |
| `resilience-patterns` | `failure-and-degradation.md` — backoff and jitter, breakers, silent fallbacks, graceful shutdown |
| `observability` | `correlation-and-signals.md` — the three signals, correlation, RED metrics, log levels |
Every skill with a reference also ships **complete, copyable implementations** under `assets/` —
not fragments:
| | |
|---|---|
| `mongoose-schema.js` · `mongoose-queries.js` | a schema passing every MNG rule; the query patterns |
| `outbox-publisher.js` · `idempotent-consumer.js` | the transactional outbox and its relay; a consumer that survives redelivery |
| `resilient-client.js` · `graceful-shutdown.js` | timeout + backoff + jitter + breaker composed; SIGTERM in the order that matters |
| `logger.js` · `config.js` | structured logging with correlation and redaction; environment validated at boot |
Assets are partitioned by language — `assets/java/`, `assets/node/`, `assets/shared/` — so adding a
language is a new directory rather than a reshuffle. Each asset's real destination
(`src/main/java/...`, `db/migration/...`, `k8s/...`) is **derived** from that, which matters because
a checker only selects a file whose path matches. See [STRUCTURE.md](STRUCTURE.md).
**Every asset is checked against the standard it demonstrates**, by a test, at the path it would
occupy in a real repository. An example that
violates the rule it teaches is worse than no example — it is the strongest signal a reader gets,
and it points the wrong way. Adding that check exposed **four rule precision bugs**:
| Rule | Was wrong because |
|---|---|
| `CFG006` | Fired on `PRICING_API_KEY: z.string().min(16)` — a schema declaration, not a secret, in the very file that validates configuration |
| `RES001` / `RES002` | Used proximity, but backoff is a helper defined once at the top of a module and used further down, so every correct implementation was reported |
| `MNG012` | `schema.index({...})` is declared past the schema body, well beyond any sane window |
| `OBS010` | A logger implementation names every level by definition |
All four now use file scope or an exclusion rather than a proximity window.
Skill descriptions also carry a `not-for` line on all 11 skills. That clause is what stops a skill
loading on every task, and it matters more than the trigger list.
## Two kinds of rule
**Code rules** live in `lib/check/*.mjs` and need real structure: the OpenAPI `$ref` walk, the SQL
DDL reader, the Kubernetes pod spec.
**Declarative rules** live in `lib/rulesets/` and are data — a pattern, the files it applies to,
and the proximity conditions under which it matters:
```js
{
id: 'RES001', skill: 'resilience-patterns', severity: 'error',
files: ['**/*.ts', '**/*.java'],
forbid: '\\b(?:retry|maxRetries)\\b',
near: { pattern: 'backoff|exponential|jitter', lines: 10 }, // suppress when mitigated
message: 'retry configured with no backoff strategy',
}
```
`near` means *"fine as long as that is close by"*; `requireNear` means *"only matters when that is
close by"*. Getting those the wrong way round makes a rule report the exact complement of what it
means, which is a mistake this package shipped once and now has a test for.
### Your own rules, without forking
The rules that matter most to you are the ones nobody else can ship. Put them in `.mlskills.json`:
```jsonc
{
"customRules": [{
"id": "ACME001",
"skill": "backend-patterns",
"severity": "error",
"files": ["**/*.ts"],
"forbid": "from ['\"]moment['\"]",
"title": "No moment.js",
"why": "Frozen upstream. Use Temporal or date-fns.",
"message": "moment is banned — see ADR-014"
}]
}
```
They run alongside the built-ins, appear in `explain`, and obey the same ratification gate: an org
cannot grant itself an error-severity rule on a standard it has not ratified. Malformed custom
rules throw at load rather than silently matching nothing — a rule that quietly matches nothing
reports a clean repo, which is the worst possible failure for a checker.
---
## Use it
### As an MCP server (agents)
**Claude Code**
```bash
claude mcp add ml-skills -- npx -y -p @mlmcps/ml-skills ml-skills-mcp --root .
```
**Cursor, VS Code, Windsurf, or any MCP client** — in the client's MCP config:
```json
{ "mcpServers": { "ml-skills": { "command": "npx",
"args": ["-y", "-p", "@mlmcps/ml-skills", "ml-skills-mcp", "--root", "."] } } }
```
**Remote / HTTP clients** — run `ml-skills-http` and point the client at `POST /mcp`
(MCP Streamable HTTP).
> One package, three binaries. `npx @mlmcps/ml-skills` runs the CLI because that bin matches the
> package name; the other two need `-p`: `npx -p @mlmcps/ml-skills ml-skills-mcp`. There is no
> separate `@mlmcps/ml-skills-mcp` package to install.
Check which copy is running — the predictable failure when a team mixes a stale npx cache with a
fresh install — with `--version`, which prints the version *and* the resolved path:
```bash
npx -p @mlmcps/ml-skills ml-skills-mcp --version
```
The server exposes three surfaces, because clients use different ones:
| Surface | What |
|---|---|
| **Tools** | `skill_list`, `skill_get`, `rule_explain`, `check_repo`, `check_content`, `ratify_status`, `adapter_status` |
| **Resources** | `mlskill://<skill>/SKILL.md`, reference examples, the rule catalogue as JSON |
| **Prompts** | `apply-<skill>` for each standard, plus `review-against-standards` |
`check_repo` reads from disk; `check_content` takes documents inline, for a file being drafted or a
client with no filesystem access. Both return the same verdicts.
Everything is **read-only** and makes **no network calls** — a standards checker that phoned home
would not survive its first security review, and it has to run in the pipeline that has no egress.
### In a website
```bash
npx -p @mlmcps/ml-skills ml-skills-http --port 8787
```
Serves a browser UI at `/` (read the standards, paste a file, see the findings) and a JSON API:
| | |
|---|---|
| `GET /v1/skills` · `GET /v1/skills/:name` | the standards, including full document text |
| `GET /v1/rules` · `GET /v1/rules/:id` | the rule catalogue and per-rule rationale |
| `POST /v1/check` | `{ files: [{ path, text }], skills?, overrides? }` → findings |
| `POST /v1/ratify` | which decisions are still shipped defaults |
| `POST /mcp` | MCP Streamable HTTP |
| `GET /health` · `GET /ready` | distinct, per this package's own infra standard |
```js
const res = await fetch('https://standards.example.com/v1/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ files: [{ path: 'openapi.yaml', text: spec }] }),
});
const { summary, findings } = await res.json();
```
The HTTP face deliberately **does not** accept a path to scan. A service that took a directory and
walked it would be a directory-traversal oracle for anyone who could reach the port. Callers send
content; disk access stays on the stdio transport, where the client already has the files.
### In CI
```yaml
- run: npx @mlmcps/ml-skills check . --json > standards.json
```
Exits `1` on any error-severity finding from a **ratified** standard, `0` otherwise.
### As a library
```js
import { check } from '@mlmcps/ml-skills';
const { findings, summary } = check(
[{ path: 'openapi.yaml', text }],
{ overrides: { 'openapi-contract': { status: 'standard' } } },
);
```
`check()` is pure — content in, findings out, no filesystem and no network — which is why the same
function backs the agent, the website and the pipeline. It runs in a browser bundle unchanged.
---
## Making it yours
```bash
npx @mlmcps/ml-skills init # writes .mlskills.json
npx @mlmcps/ml-skills ratify # what nobody has signed off yet
```
```jsonc
{
"openapi-contract": {
"status": "standard", // now it can fail a build
"decisions": {
"pagination": "offset", // we disagree with the default, and that is fine
"paginationParams": ["offset", "limit"],
"versionPattern": "^/api/v[0-9]+/"
},
"severity": { "OAS002": "off" } // we do not care about summaries
},
"ui-patterns": { "status": "proposed" }
}
```
Arrays replace rather than merge — an override of a list means *instead of*, not *as well as*.
Exclude paths with a top-level `exclude` (or per skill). Deliberately non-compliant fixtures are
the common case, and a checker you have to ignore is a checker nobody reads:
```jsonc
{ "exclude": ["test/fixtures/**", "**/*.generated.ts"] }
```
This repo ships its own `.mlskills.json` doing exactly that, with all five standards ratified —
`npx @mlmcps/ml-skills check .` on this package reports zero findings, and the two rules it turns
off say in the file why.
**Ratifying means three things:** someone read the decision, someone agreed to it, and someone
accepts that it can now break a build. See [RATIFY.md](RATIFY.md) for the checklist and the
decisions that most need a human.
---
## Honest limitations
- **Most built-in checks are text-based, not AST-based.** Five standards have structural checkers;
the rest are patterns over the right files with proximity conditions. For AST-level matching, run
the Semgrep or ESLint adapter — that is what they are for.
- **Nothing built in inspects your dependencies.** A clean run says nothing about CVEs in your
lockfile. Use the Trivy adapter. Supporting TypeScript, JavaScript,
Java and Kotlin with real parsers would be four dependencies and four things to keep current, and
this package has none. What is checked is what survives being checked on text: import edges,
forbidden calls, credential and log shapes. Type-level rules belong in your language's own
linter, and this does not replace it.
- **The YAML reader is a subset.** Block mappings, sequences, flow collections, block scalars and
comments — enough for OpenAPI, k8s and compose. It does not resolve anchors or aliases, and says
so in the findings rather than parsing them wrongly.
- **These checks find shapes, not semantics.** `check` passing does not mean the design is good; it
means it does not violate the specific things you wrote down. Design review is still a human job.
- **False positives exist and are reported as findings, not truths.** Silence a rule with
`"severity": { "RULE": "off" }` rather than working around it.
---
## Development
```bash
npm test # 83 tests, no dependencies, no network
npm run eval # behavioural eval scenarios
npm run serve # HTTP + web UI on :8787
npm run mcp # stdio MCP server
```
The reference example in `skills/openapi-contract/assets/endpoint.yaml` is checked by the test
suite against its own standard. If a rule changes and the example stops passing, the build fails —
a standard whose own example violates it is worse than no standard.
MIT. `@mlmcps/ml-skills`.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues