Agents
A coding assistant meets jscpd in three places. As an MCP server, jscpd runs inside the client (Claude Desktop, Claude Code, Cursor, Copilot and others) and answers questions about the project it scanned: whether a snippet already exists, which clones a file takes part in. As a set of skills, it gives the agent tested workflows that run the command line, from removing clones to porting a library. And whatever starts jscpd, the ai reporter prints the result in the fewest tokens.
jscpd on the PATH of the machine the client runs on (Installation). The skills install with npx skills add and run jscpd through npx jscpd, so they need Node.js; compare-codebases and code-migration came with jscpd 5.4.0. The ai reporter has been part of jscpd since 4.1.0.| You want | Use |
|---|---|
| The assistant checks code against the project while it writes, inside the editor | the MCP server |
| The assistant runs a whole task, such as removing the clones or porting a library | a skill |
| A script or a prompt reads a jscpd report | --reporters ai |
The MCP server
Connect a client
jscpd speaks the Model Context Protocol over stdio. The client starts the process and talks to it on stdin and stdout, so there is no port and nothing to install besides jscpd. Claude Desktop, Claude Code and Cursor read the same shape of configuration:
{
"mcpServers": {
"jscpd": {
"command": "jscpd",
"args": ["--mcp", "/path/to/project"]
}
}
}
| Client | Where the configuration lives |
|---|---|
| Claude Desktop | ~/Library/Application Support/Claude/claude_desktop_config.json on macOS, %APPDATA%\Claude\claude_desktop_config.json on Windows |
| Claude Code | .mcp.json in the project, or one command: claude mcp add --scope project jscpd -- jscpd --mcp /path/to/project |
| Cursor | .cursor/mcp.json in the project, or ~/.cursor/mcp.json for every project |
| VS Code with Copilot | .vscode/mcp.json, with the shape below |
{
"servers": {
"jscpd": {
"type": "stdio",
"command": "jscpd",
"args": ["--mcp", "/path/to/project"]
}
}
}
Give the project as an absolute path: the MCP documentation asks for absolute paths in Claude Desktop's configuration, and an absolute path works in every client. The detection options of a normal run apply to the scan and to every snippet check, so pass the ones your CI uses and the answers match its reports:
jscpd --mcp --min-tokens 30 --format javascript,typescript /path/to/project
jscpd scans the paths once at start, reports the scan on stderr and then waits for the client. stdout carries protocol messages only.
jscpd MCP server (stdio): scanned 12 files, 6 clones in 20ms — waiting for client
Try it from a shell
The server reads one JSON-RPC message per line, the way a client sends them. On fixtures/type3-demo of the jscpd repository, this asks for the clones of one file:
cd fixtures/type3-demo
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"shell","version":"0"}}}' '{"jsonrpc":"2.0","method":"notifications/initialized"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_file_clones","arguments":{"path":"inserted-line/save-user.js"}}}' | jscpd --mcp . 2>/dev/null
The first line of the answer is the initialize result, with "protocolVersion": "2025-06-18" and serverInfo (jscpd, version 5.4.0). The second is the tool result, whose text content is this JSON:
{
"clones": 2,
"duplications": [
{
"endA": 12,
"endB": 11,
"fileA": "inserted-line/save-account.js",
"fileB": "inserted-line/save-user.js",
"format": "javascript",
"lines": 6,
"startA": 6,
"startB": 5,
"tokens": 100
},
{
"endA": 6,
"endB": 6,
"fileA": "inserted-line/save-account.js",
"fileB": "inserted-line/save-user.js",
"format": "javascript",
"lines": 5,
"startA": 1,
"startB": 1,
"tokens": 60
}
],
"file": "inserted-line/save-user.js",
"returned": 2
}
Reading the answers
Every tool answers with compact JSON in one text content item. Clone lists come biggest first and are cut at limit entries, 100 unless the call says otherwise; the count next to a list (clones, count) is always the whole number, and a cut list carries a note that says so. Paths are relative to the scan root, in the form the other tools accept. When a call cannot use its arguments, such as a missing code or an unknown format, the server answers with a tool error (isError: true) and a message that says what to fix:
{"error": "unknown format 'klingon': run `cpd --list` for supported formats"}
An unknown tool name gets a JSON-RPC error. All four tools are read-only and never change the project's files. The initialize result also carries instructions, a short text that tells the assistant when to call which tool.
Check a snippet before writing it
check_duplication compares a snippet with the scanned project, so the assistant can reuse the existing copy. With the body of inserted-line/save-account.js as code and "format": "js", the demo answers:
{
"count": 1,
"duplications": [
{
"file": "inserted-line/save-account.js",
"fileEndLine": 12,
"fileStartLine": 1,
"snippetEndLine": 12,
"snippetStartLine": 1,
"tokens": 172
}
],
"format": "javascript",
"returned": 1
}
A snippet shorter than --min-tokens cannot match and says why instead of answering with an empty list. For let x = 1;:
{
"count": 0,
"duplications": [],
"format": "javascript",
"note": "snippet has 5 tokens, below the detection threshold of 50 (--min-tokens)"
}
With "similarity": 0.85 the answer also lists project functions whose syntax-tree shape resembles each function of the snippet, under similar with a similarCount; each entry has the file, its line range, the project function's name, the snippetName and a similarity ratio. This part works for JavaScript and TypeScript only, and 1 turns it off.
See a file's clones before refactoring it
get_file_clones lists every clone of one file, as the shell example above shows. The file you ask about can be fileA or fileB of a clone. A path that was not part of the scan answers with clones: 0.
Scan again after the assistant edits files
The other tools answer from the scan made at start. check_current_directory scans the configured paths again, replaces that scan and returns the new counts and clone list. With "limit": 2 on the demo:
{
"clones": 6,
"duplicatedLines": 42,
"duplications": [
{
"endA": 15,
"endB": 14,
"fileA": "long-line/theme-branded.js",
"fileB": "long-line/theme-default.js",
"format": "javascript",
"lines": 7,
"startA": 8,
"startB": 7,
"tokens": 117
},
{
"endA": 12,
"endB": 11,
"fileA": "inserted-line/save-account.js",
"fileB": "inserted-line/save-user.js",
"format": "javascript",
"lines": 6,
"startA": 6,
"startB": 5,
"tokens": 100
}
],
"files": 12,
"note": "clone list truncated to 2; pass a higher 'limit' for more",
"percentage": 9.051724137931034,
"returned": 2
}
get_statistics returns the totals of the last scan and the same numbers per format, so the assistant can compare a project before and after a refactoring. Its statistics.total on the demo:
{
"clones": 6,
"duplicatedLines": 42,
"duplicatedTokens": 528,
"lines": 464,
"newClones": 0,
"newDuplicatedLines": 0,
"percentage": 9.051724137931034,
"percentageTokens": 11.865168539325843,
"sources": 12,
"tokens": 4450
}
Tools
| Tool | Arguments | Answer |
|---|---|---|
check_duplication | code (required), format (required; a format name such as javascript or an extension such as js), limit, similarity (a ratio in (0, 1]) | format, count, returned, duplications[] with file, fileStartLine, fileEndLine, snippetStartLine, snippetEndLine, tokens; with similarity below 1 also similar[] and similarCount |
get_file_clones | path (required; relative to the scan root or absolute), limit | file, clones, returned, duplications[] with format, fileA, startA, endA, fileB, startB, endB, lines, tokens |
get_statistics | none | files, clones, statistics with total and one object per format: sources, lines, tokens, clones, duplicatedLines, duplicatedTokens, percentage, percentageTokens |
check_current_directory | limit | files, clones, duplicatedLines, percentage, returned, duplications[] in the shape of get_file_clones |
The server answers initialize with protocol revision 2025-06-18 and also accepts clients of 2025-03-26 and 2024-11-05; a request for any other revision gets 2025-06-18.
The skills
A skill is a folder with a SKILL.md that an assistant reads when a task matches its description, so the agent runs jscpd with the right options and follows a tested workflow. jscpd ships five of them for skills.sh; the sources are in skills/ of the repository.
npx skills add kucherenko/jscpd
npx skills add kucherenko/jscpd --skill dry-refactoring
The first line installs all five, the second one of them. Every skill runs jscpd through npx jscpd, so the agent needs no global install. Once a skill is installed, a request in its words is enough: "find and fix code duplication" runs the first two, "clean up this codebase" the third, "port this library to Rust" the last two.
| Skill | What it does | When the agent needs it |
|---|---|---|
jscpd | The reference for jscpd itself: the flags, the passes for each kind of clone, the ai reporter's format, the config file | Any run of jscpd |
dry-refactoring | Removes the clones jscpd found: reads each pair, triages it, picks a strategy, applies it, checks the clone is gone | "Find and fix code duplication" |
codebase-refactoring | A pass over duplication, dead code and complexity, measured with --health before and after | "Clean up this codebase", "pay down tech debt" |
compare-codebases | Compares two folders function by function with --compare and explains the result (5.4.0+) | "How far is the port?", "What does the Android app have that iOS lacks?" |
code-migration | Ports a codebase to another language or framework, tests first, with --compare as the progress measure (5.4.0+) | "Port this Python library to TypeScript" |
jscpd
The reference the other skills build on. It tells the agent to scan with the ai reporter, to scope a run with --ignore and --format, and how to read each line of the report. It describes the default scan, which reports exact copies, and two noisier passes to run after those are dealt with: --ignore-identifiers --min-tokens 70 for copies that differ only in names and values, and --max-gap-lines 1 --similarity 0.85 for copies with a few edited lines or the same function structure. It also covers --summary for a hotspot ranking, --complexity for the complexity ranking alone, and the keys of .jscpd.json.
dry-refactoring
The workflow for removing clones. The agent runs jscpd with --reporters ai, reads both fragments of each clone, works out what the code does, and triages renamed and similar clones before touching them: a pair whose two sides do different things, intentional boilerplate, generated code, a pair that would need a vague name, or a pair under about ten lines is skipped and reported as skipped. The kind of clone picks the strategy, and the agent runs jscpd again with the same flags to confirm the clone is gone, highest impact first.
| Strategy | When |
|---|---|
| Extract function | The duplicate is a block of logic |
| Extract module or utility | The duplicate spans files in different domains |
| Extract constant or config | The duplicate is repeated data or configuration |
| Template or base class | The duplicate is a repeated class shape |
| Parameterize | A (renamed) clone: the same algorithm over different names or values |
| Unify near-miss copies | A [gap] clone: extract the common body and pass the divergence in |
| Merge similar functions | An [ast] clone: extract the shared skeleton and inject what differs |
Two of its tips save time on real projects. A clone between a .js and a .ts file, found with --cross-formats, usually means code was ported without deleting the original, so the fix is one implementation. Many (renamed) clones in one file usually point at one missing abstraction, and many across test files usually mean nothing, since test cases are supposed to look alike.
codebase-refactoring
Starts from one number and works where it is lowest:
npx jscpd --health --reporters ai ./src
health 74 B (duplication 74, dead-code 72, complexity 76; 93 code lines)
The health score is a weighted mix of three sub-scores, and the lowest one names where the codebase hurts most. The skill then makes three passes in the order duplication, dead code, complexity: dry-refactoring for the clones, --dead-code for unused files, exports and imports (removed, or kept with a reason), and --complexity for the largest and most complex files, which get split or simplified. The order matters because an extraction moves call sites the dead-code pass would otherwise trace, and removed dead code changes which files rank as complex. After each pass the agent runs --health again and reports the change.
compare-codebases
jscpd --compare A B pairs every function of folder A with the function of folder B that does the same job, in one language or across two, and lists the functions that have no counterpart. The skill explains how the pairing works, by code with the embedding model and then by name, and what the high, medium and low levels mean. Its six steps: point at the code and keep vendored and generated files out with .gitignore or --ignore; ask before the one-time npx jscpd --semantic-download (548 MB); read the overview; check a sample of pairs and every low one; search the other folder for an unmatched function before calling it missing; and report the two percentages, the renamed pairs and the real gaps, with the HTML migration map for a reader who wants to explore.
npx jscpd --compare python/ typescript/
npx jscpd --compare ios/ android/
The first line measures a port, source first and target second; the second checks two implementations of one app for parity. Comparing two codebases describes the reports.
code-migration
The port workflow, with --compare as the progress measure and the source's tests as the definition of done. The agent first builds a map from each source function to the tests that cover it, from a per-test coverage report, so a function is ported together with the tests that prove it. Then it ports the tests, keeping inputs and expected values, and keeps them out of the build until their functions land; it writes no stubs, because a stub under the source's name can pair by name and count as ported. Then it ports the code one function at a time, starting from the report's readyToPort list (unmatched functions whose callees already have a counterpart), turns on each function's tests as it lands, checks with coverage that they reach the new code, and reruns the report after each batch. Progress is reported as two numbers, tests ported and passing, and functions ported, with the functions left out on purpose and why.
The skill was tested by porting fs-extra 11.4.1 to a Rust addon. The port is on npm as @jscpd/fs-extra, with the code at kucherenko/fs-extra-rs; a run of the same task without the skill is kept at kucherenko/fs-extra-rs-plain for comparison.
The ai reporter
The ai reporter prints one line per clone and a summary, with no code fragments and no colors. On fixtures/type3-demo:
jscpd . --reporters ai
Clones:
inserted-line/ save-account.js:1-6 ~ save-user.js:1-6
inserted-line/ save-account.js:6-12 ~ save-user.js:5-11
long-line/ theme-branded.js:1-8 ~ theme-default.js:1-8
long-line/ theme-branded.js:8-15 ~ theme-default.js:7-14
wide-gap/ place-order-guarded.js:1-6 ~ place-order.js:1-6
wide-gap/ place-order-guarded.js:9-15 ~ place-order.js:6-12
---
6 clones · 9.1% duplication
time: 5.439ms
On this demo the console reporter prints 2,099 characters and the ai reporter 394. On the repository's fixture corpus of 212 clones, the benchmark measured about 2,800 tokens for the ai reporter against about 23,000 for the console reporter, and compares it with other detectors.
Reading a clone line
The two fragments stand on either side of ~. A directory the two files share is written once, followed by a space, so inserted-line/ save-account.js:1-6 ~ save-user.js:1-6 names inserted-line/save-account.js and inserted-line/save-user.js. A clone within one file gives the path once and two ranges: file.js 10-25 ~ 45-60. No suffix means an exact copy. The passes that find the other kinds add one:
identifiers/ basket.js:1-9 ~ cart.js:1-9 (renamed)
inserted-line/ save-account.js:1-12 ~ save-user.js:1-11 [~0.91 gap]
renamed-halves/ sync-contacts.js:1-13 ~ sync-leads.js:1-14 [~0.88 ast]
(renamed) is a copy that differs only in identifiers, literals or annotations, reported with --ignore-identifiers, --ignore-literals or --ignore-annotations. [~0.91 gap] is two exact clones merged across up to --max-gap-lines unmatched lines; the number is the share of matched tokens over the merged span. [~0.88 ast] is a pair of JavaScript or TypeScript functions whose syntax trees overlap at least --similarity; names and literal values do not count. Types of code clones explains the kinds.
With the other modes
The reporter keeps the same shape in the other modes, measured here on fixtures/dashboard-demo:
jscpd . --reporters ai --summary --summary-top 3
Clones:
src/ checks.ts:8-13 ~ labels.ts:4-9
---
1 clones · 1.9% duplication
---
Summary by tokens (6 files, 3 folders):
files (tokens/lines/size/cx/dup%):
README.md 1289/163/8.0K/0/0.0%
src/rates.ts 182/35/757/11/0.0%
src/checks.ts 169/21/665/6/28.6%
folders (files/tokens/lines/size):
. 1/1289/163/8.0K
src 4/557/83/2.2K
src/legacy 1/113/10/430
time: 4.073ms
jscpd . --reporters ai --complexity --summary-top 3
Complexity by complexity (6 files, 3 folders):
files (tokens/lines/size/cx):
src/rates.ts 182/35/757/11
src/checks.ts 169/21/665/6
src/labels.ts 109/11/409/3
folders (files/tokens/lines/size/mean cx):
src 4/557/83/2.2K/5
src/legacy 1/113/10/430/3
. 1/1289/163/8.0K/0
time: 3.009ms
jscpd . --reporters ai --dead-code
basta dead code report — 2 findings across 5 files (14.0% of 93 lines)
Confidence is 0-100; anything below 90 has a listed reason it may be wrong. Verify a finding before deleting the code.
unused-file src/legacy/manifest.ts:1:1 confidence=95 src/legacy/manifest.ts is never imported and is not an entry point
unused-export src/checks.ts:19:17 confidence=85 exported function `checkBatch` is never imported
--health --reporters ai prints the one-line form shown under codebase-refactoring. To hand an agent one kind of clone, combine --reporters ai with --kind, for example --ignore-identifiers --kind renamed for the renamed copies alone. Reporters lists every reporter and the files the others write.
Limits
- The MCP tools answer from the last scan. After the assistant edits files,
check_current_directoryis the call that refreshes them; it scans synchronously and can take seconds on a large project. check_duplicationneeds a snippet of at least--min-tokenstokens (50 by default) and compares tokens, so it finds exact copies and, with the server's normalization options, renamed ones. Thesimilarityargument works for JavaScript and TypeScript only.- The MCP server has no HTTP transport.
jscpd-server, MCP over HTTP with a REST API, belongs to v4 and is maintained on themaster-v4branch; see v4. - The port skills need the embedding model or an embeddings API, and the first comparison of two folders embeds every function, which can take minutes. Semantic clones covers the model.
Related
- Editors shows the same findings as diagnostics while you type, through
jscpd --lsp. - Reporters lists the reporters and the files they write.
- Comparing two codebases explains the reports the two port skills read.
- Dead code covers the second pass of
codebase-refactoring.
Cross-format detection
Find clones between JavaScript and TypeScript files with --cross-formats, and inside Vue, Svelte, Astro and Markdown files, whose blocks are scanned under their own formats.
Editors
Run jscpd --lsp as a language server so Neovim, Helix, Sublime Text, Emacs, JetBrains IDEs and VS Code show clones, dead code and complexity as diagnostics while you type.