Skip to main content
Glama

slop-scorer-mcp

An MCP server that tells a coding agent, with citations, which parts of a codebase or a rendered web page read as machine-generated template output, and hands back the exact edit that would remove each one.

No model is involved anywhere in this server. Every finding cites a file and a line number, or a CSS selector and a computed style value, that you can go and check yourself. Seventy rules, all of them data, all of them carrying their own rebuttal.

claude mcp add slop-scorer -- npx -y github:Yuve21/slop-scorer-mcp

Node 20 or newer and nothing else. Not on npm yet, so that line installs from this repository; the first launch compiles and takes about a minute. Full detail, and the two other ways in, are under Install.


Why this exists

The interesting half is not detection. It is list_rules, which returns the whole corpus so an agent can read what not to produce before it produces anything.

Detection is a race that eventually gets lost: generators improve, tells decay, and a corpus of tells is a depreciating asset. Prevention does not decay, because an agent that reads the corpus first writes work the corpus does not fire on. It is the only loop here that gets stronger the more it is used.

Related MCP server: Sensory-Grounding MCP

What it will not do

This matters more than the feature list, so it is first.

  • It never says a person used AI. Every sentence it can emit has the same grammar: the subject is the server, the object is the artifact, the verb is something the server did. "We checked this artifact against 15 deterministic rules and 4 matched, on 88% coverage." Not "this is AI." The formatter cannot express the other sentence, and a test in @slop/core fails the build if a forbidden phrase appears in the source.

  • Abstention is a result, not a failure. status is assessed, inconclusive (we looked and will not publish a number) or not_assessed (we did not look). Both abstentions carry score: null and a coded reason. They are not low scores and they are not clean bills of health. Branch on status, never on score.

  • The score stops at 99. There is no arrangement of evidence that reaches certainty.

  • A thin read withholds the score entirely. A page that only half rendered looks exactly like a clean page, and printing a confident number over a partial read would be the most dishonest thing this server could do.

  • It publishes no accuracy figure, anywhere. packages/core/test/no-claims.test.ts scans every shipped source file and every README on each test run and fails the build if one appears. An accuracy claim about an inference is a substantiation-bearing claim under FTC Act section 5 (In re Workado, 2026), so the only defensible way to hold one is to compute it from a named corpus at test time. If you want a number, run npm run backtest and read your own.

  • It never writes to anything it scanned. No exec, no request for write access, and no code path that modifies the artifact. Fixes come back as data for your agent to apply under your normal approval flow. The package contains exactly one filesystem write, the local observation log described below, and it is off until you switch it on, writes only to a directory you named, and never touches the target.

The local observation log (off by default)

Nothing here phones home. There is no telemetry, no usage beacon, no update check and no upload path in any package, and there is no code that could be pointed at one: the sink type takes a directory and cannot express a URL, so a future caller cannot quietly supply one.

If you want to help the corpus get better, you can switch on a local log:

SLOP_OBSERVATIONS_DIR=/an/absolute/path/you/chose

Unset, nothing is written and no file is created. That is the default and there is no remote default behind it. Set, each scan appends one JSON line to observations-YYYY-MM.jsonl in that directory, recording the shape of the read and nothing else: the corpus version, a day rather than a timestamp, the modality, the status and band, a score bucket rather than the score, the coverage ratio, which rules fired, which rules were evaluated and did NOT fire, the per-probe denominators, and a size bucket with markers from a closed vocabulary.

It records no path, no URL, no hostname, no file name, no dependency name and no observed value out of any finding. The record is built by allowlist, field by field, rather than by stripping fields, because a denylist fails open for every field somebody adds later. A guard then runs on the finished record before it reaches disk, and it REFUSES rather than sanitises: a rule id the corpus does not know, an extension that is not a bare extension, a marker outside the vocabulary or a timestamp where a day belongs throws instead of being quietly rewritten.

The file is newline-delimited JSON. Read it, grep it, delete it. Getting an observation off your machine is your job and nobody else's: there is no service to send it to.

Install

slop-scorer-mcp is not on the npm registry yet, so every command here installs from this repository. That works fine, with one caveat worth reading before you decide the install broke.

The first launch compiles from source. Measured cold, with an empty npm cache: 69 seconds. Most MCP clients give a server 30 seconds to answer, so the first connect can report connection timed out even though nothing is wrong. Reconnect, or warm the cache first:

npx -y github:Yuve21/slop-scorer-mcp   # wait for "listening on stdio", then Ctrl-C

Every launch after that is off the cache and immediate.

Claude Code

claude mcp add slop-scorer -- npx -y github:Yuve21/slop-scorer-mcp

Claude Desktop

~/Library/Application Support/Claude/claude_desktop_config.json on macOS, %APPDATA%\Claude\claude_desktop_config.json on Windows. Restart the app afterwards.

{ "mcpServers": { "slop-scorer": { "command": "npx", "args": ["-y", "github:Yuve21/slop-scorer-mcp"] } } }

Cursor

~/.cursor/mcp.json globally, or .cursor/mcp.json per project. Same block. Reload the window.

{ "mcpServers": { "slop-scorer": { "command": "npx", "args": ["-y", "github:Yuve21/slop-scorer-mcp"] } } }

From a checkout

Use this if you are going to change a rule and watch it fire.

git clone https://github.com/Yuve21/slop-scorer-mcp
cd slop-scorer-mcp
npm install                                            # `prepare` builds it for you
npm run install:local --workspace=packages/mcp-server

That last line registers the built binary with claude mcp add by absolute path. If the claude CLI is not on your PATH it prints the JSON block to paste by hand instead of failing silently.

Once it is on npm

claude mcp add slop-scorer -- npx -y slop-scorer-mcp

No build step, no cold start, because a registry tarball ships compiled. This 404s today.

How the GitHub install works

npx -y github:... resolves the repository ROOT, and npm has no way to point a git spec at a subdirectory. So the root package.json here carries a bin (a single entry, which is what lets npx pick an executable), a prepare script (the only lifecycle hook npm runs for a git dependency: prepack is not run there), a files list naming packages/mcp-server/dist (because dist/ is gitignored and npm falls back to .gitignore when packing, which would ship the tarball with the binary removed), and the two runtime dependencies the esbuild bundle leaves external. A root .npmignore sits beside them so .gitignore is never consulted at all.

scan_ui needs a browser

npx playwright install chromium

Without it, scan_ui returns status: "not_assessed" and says why. It will not fall back to reading the server HTML: a fetch-only read produces confident findings about a document nobody sees, which is how a heading check once reported "this route has 0 H1s" for a route whose heading does not exist until hydration.


The five tools

tool

what it does

list_rules

The whole corpus, compact. Read before you generate.

scan_codebase

Static analysis of a checkout, plus git history.

scan_ui

Renders a URL or a dev server in Chromium and measures the rendered document.

propose_fixes

Re-scans and returns each finding as a precise, caveated edit.

verify_fix

Re-scans and puts the before and after finding sets side by side.

list_rules

Seventy rules across two corpora: 51 web, 19 code. Compact by default, because a tool that asks to be called before you generate anything cannot cost a fifth of a small context window.

index         every rule, one line: id | family | polarity/severity | weight | rationale
fullEntries   why it reads as generated, the counter-evidence that rebuts it, how to avoid it
retrieval     a sentence on every response saying how to get what was left out
modality: "web" | "code" | "all"                     optional
family:   e.g. "agent-artifact"                      optional
ruleIds:  ["craft.no-og-image", ...]                 full entries for these only
verbose:  true, full entries for every rule          costs roughly 18k tokens

The usual pattern after a scan is ruleIds: [...the ids that fired], which costs a few hundred tokens and returns exactly the rebuttal and the prevention note you need to act on them.

scan_codebase

path:        absolute path to the repository root
include:     optional patterns, e.g. ["src/**/*.ts"]
readHistory: read git history for the commit-shape rules (default true)
maxFiles:    cap on files walked (default 5000)

A real reading, produced by pointing this server at its own checkout. Nothing here is typed by hand: it is the response bytes, lightly trimmed for length. Note that it finds things in us.

SLOP RECEIPT  20 / 99   band: few known signals
corpus code-corpus-2026.09  |  coverage 100%  |  families fired 2
evidence kind: deterministic  |  detectors: code.static-rules

We checked this artifact against 19 deterministic rules from code-corpus-2026.09 and 4
matched, on 100% coverage. That places it in the "few known signals" band at 20 of 99.
Every match below cites what we read and where.

     15  base rate before any evidence
    +26  scaffold.placeholder-markers  [scaffold-residue / medium]  -> 41
         Unfilled placeholders left in shipped source
         evidence: line packages/detectors-code/src/artifact.ts:73 = "lorem ipsum"
                   (expected "a real value")
         evidence: line packages/detectors-code/src/artifact.ts:109 = "TODO: implement"
                   (expected "a real value")
         evidence: line packages/detectors-code/src/suppression.ts:18 = "lorem ipsum"
                   (expected "a real value")
         ... and 4 more, 5 counted toward the weight
         caveat: TODOs are a normal and healthy way to record known gaps, and a mature
                 codebase carries plenty. Markers that name an owner or a ticket are
                 excluded as tracked work. Suppressed by suppress.self-defining-pattern:
                 19 match(es) were withdrawn as self-referential: the cited lines in
                 packages/detectors-code/src/scan.ts are the DEFINITION of the patterns
                 that matched them, not uses of them.
    +23  verify.tautological-tests  [verification-floor / high]  -> 64
         Tests that cannot fail
         evidence: file packages/detectors-code/test/corpus/index.ts
                   = "93 lines, 0 assertions" (expected "at least one assertion")
         evidence: file packages/detectors-web/test/corpus/index.ts
                   = "60 lines, 0 assertions" (expected "at least one assertion")
         caveat: A deliberate smoke test that only checks a module imports without
                 throwing looks like this, and an assertion helper the scanner does not
                 recognise reads as zero assertions.
    -42  counter.rationale-comments  [counter-evidence / counter]  -> 22
         Comments that record a reason, not a restatement
         evidence: line packages/core/src/assessment.ts:61 = "We read the artifact cleanly
                   and it carried no declared provenance either way. ABSENCE OF PROVENANCE
                   IS NOT EVIDENCE OF GENERATION."
         ... and 3 more, 4 counted toward the weight
         caveat: A rationale comment can be copied along with the code it explains, and an
                 agent prompted to explain its reasoning will write plausible ones. The
                 signal is real but it is not proof.
     -2  counter.real-test-coverage  [counter-evidence / counter]  -> 20
         Tests dense enough to actually fail
         evidence: metric "26 test files against 63 source files"
                   = "606 assertions, 0 tautologies"

     20  TOTAL (base 15 + 4 contribution(s))

FAMILY CAPS
  Scaffold residue             +26 pts   logit 1.37 of 1.60 allowed
  Verification floor           +23 pts   logit 1.35 of 0.96 allowed  AT CAP
  Global counter-evidence      logit -2.00  AT CAP  (bypasses family caps)

WHAT WOULD CHANGE THIS SCORE
  -26  scaffold.placeholder-markers: Fail the build on the placeholder strings your
       scaffold ships with. They are the ones nobody notices.
  -23  verify.tautological-tests: Mutation-test the test: break the code it covers and
       confirm it goes red. If it stays green, delete it or fix it.

A high score means this artifact resembles generated-template output. It is not proof that
a tool made it, and it is not a judgement of the person who made it.

Four things in that output are worth pointing at, because they are the design:

  1. Counter-evidence subtracts. Two of the four matches argue for the artifact and together they are worth more than everything against it. A corpus that could only accuse would score every careful codebase as generated.

  2. Families are capped. "Verification floor" contributed 23 points and is marked AT CAP, so no single correlated group of tells can carry a verdict alone.

  3. The receipt reconciles. Base rate plus every printed contribution equals the printed score, in integers, exactly. You can recompute the number by hand.

  4. A suppressor fired and said so. Nineteen placeholder matches were withdrawn because the lines that matched are the pattern table that defines the pattern. A phase-two suppressor may withdraw evidence about the detector rather than about the artifact, but it has to name the rule, the file and the reason, in the receipt, where you can see it.

In the real response every value copied out of the scanned artifact is wrapped in an untrusted fence and the payload closes with a prompt-injection warning, because a scanner that quotes a stranger's source into an agent's context is an injection vector. The fences are stripped above only so the example reads.

scan_ui

url:            https://example.com/pricing
port:           3000                      (instead of url, for a local dev server)
viewportWidth:  default 390
viewportHeight: default 844

Renders in Chromium and measures the rendered document: builder fingerprints, default visual language, craft-floor defects, structural and motion uniformity, copy tells. A real finding:

   +7  css.crushed-tracking  [visual-default / medium]  -> 34
       Headline letter-spacing is crushed at a heavy weight
       evidence: css letter-spacing on h1 = "-0.04em at weight 800"
                 (expected "-0.02em or looser at weight 700+")
       caveat: Some faces genuinely want negative tracking at display size, and a
               type-literate designer may choose exactly this. It is a taste signal,
               not a provenance signal.

One capability is missing from this build and it is missing on purpose. Text rendered inside an image is not recovered here: the recoverer lives in a private half of the project, so packages/ocr-text in this repository always abstains, with a stated reason attached to every image it was asked about. The three imgtext.* rules therefore cannot fire on a live scan_ui run. They remain in list_rules (the advice not to bury your copy in a JPEG is worth reading either way) and they still score the frozen corpus artifacts, so archived numbers do not move. This is written down rather than quietly omitted because an unread image is not an image with no words in it.

propose_fixes

Re-scans the target and returns every finding as a proposal, grouped by family and split four ways so your agent can present it as "apply these N, skip these M".

bucket

meaning

readyToApply

replace_range, insert, replace_file. Locator and replacement both determined.

needsConfirmation

delete_file. The only destructive kind, and its own bucket so it can be gated.

needsSourceLocation

ui_change. Selector, property and values are exact; the file that declares them is not knowable from a rendered read.

decideYourself

manual. A person decides. The locator and what a good answer looks like, and no invented value.

{
  "id": "agent.instruction-file-committed#1",
  "ruleId": "agent.instruction-file-committed",
  "applicability": "confirm",
  "destructive": true,
  "blastRadius": "file",
  "remediation": {
    "kind": "delete_file",
    "path": "CLAUDE.md",
    "bytes": 4200,
    "summary": "Remove CLAUDE.md from the repository and from the index.",
    "rebuttal": "This says how the repository was worked on, not who wrote any given line.",
    "doNotApplyIf": "this file is a deliberate part of how the team works. In that case keep it and say so in the README, which answers the finding without deleting anything."
  }
}

Most rules propose manual on purpose. A page title, a meta description, an alt attribute and a brand palette are all things a machine can produce instantly, and all things whose machine production is the defect this corpus measures. Generated alt text is the clearest case: it satisfies the checker and tells a screen reader user, confidently, about an image nobody looked at. So those rules name the gap and stop.

Two constraints hold everywhere in this tool:

  • Only a deterministic read may propose a patch. Where a detector abstains from certainty, the fix cannot assert it.

  • Every fix carries the rule's own rebuttal and an explicit doNotApplyIf, injected from the rule's falsePositiveNote so the fix and the argument against it can never drift apart.

  • Counter-evidence is never remediable. A counter finding argues for the artifact. The corpus throws at load if anyone attaches a fix to one.

verify_fix

Before: 9 finding(s). After: 6. 3 no longer present, 6 still present, 0 newly present.
Score moved by -14 point(s).

It does not report success. noLongerPresent is a list of rules that no longer match at those locators, which is a fact about a re-scan and not a claim that a problem was solved. newlyPresent is stated first and sets regression: true, because a change that resolves two findings and introduces one has broken something, and a verifier that reported the net would call that an improvement. If no earlier reading of the target is held in this session it says so rather than comparing against nothing: a first run is never an all-clear.


The loop

scan, propose, apply, verify. The report was never the point; the change was.

1. scan_codebase / scan_ui   what is there, with a locator on every claim
2. propose_fixes             the same findings as precise, caveated edits
3. YOUR agent applies them   with its own edit tools, under the user's normal approval
4. verify_fix                re-scan, and show which findings are no longer present

Step 3 is deliberately not this server's job. Your agent already has edit tools, an approval prompt and a user who trusts them. A second, worse copy of that machinery inside an MCP server would be more code, more risk and less control.


What is in this repository

package

what it is

packages/mcp-server

The server. Five tools, the stdio binary, the publish-shape bundle.

packages/core

The rule contract, the scoring engine, the receipt, the calibration harness.

packages/detectors-code

The code corpus: 19 rules, plus 16 labelled repositories (10 human, 4 generated, 2 synthetic).

packages/detectors-web

The web corpus: 51 rules, plus 9 labelled pages (5 human, 4 generated).

packages/ocr-text

The abstaining image-text stand-in described under scan_ui.

The published npm package is self-contained: npm run build bundles the three internal workspace packages into dist/bin.js and dist/index.js with esbuild and vendors their type declarations, so nothing in the tarball points at a package that was never published. @modelcontextprotocol/sdk and zod stay real external dependencies; playwright stays an optional peer.

Build it yourself

npm install
npm run build      # tsc -b, then the esbuild bundle
npm test           # the rule suite, both calibration corpora, the security and FTC guards
npm run backtest   # the gate: replay both frozen corpora and diff against the baseline

npm test never launches a browser. The web tests run against stored artifacts, which is also what makes the calibration reproducible.

Contributing

Read CONTRIBUTING.md first. The short version: a rule in this repository is an accusation with arithmetic behind it, so the bar is npm run backtest, not "the tests are green". Every rule ships a positive fixture that must fire and a mutated fixture that must not, and the meta-suite runs both on every test run. Adding a corpus member with stated, checkable provenance is a bigger contribution than adding a rule.

Licence

MIT. See LICENSE.

Maintenance

ActivityMaintained
ResponsivenessSyncing

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Yuve21/slop-scorer-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server