Skip to content

Commit 67483f0

Browse files
committed
Improve mcp guidance
1 parent 55e3f3b commit 67483f0

5 files changed

Lines changed: 204 additions & 47 deletions

File tree

packages/knip/src/reporters/util/configuration-hints.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,16 @@ interface ProcessedHint extends ConfigurationHint {
9999
message: string;
100100
}
101101

102+
const UNCONFIGURED_MIN_FILES = 20;
103+
const UNCONFIGURED_MIN_RATIO = 0.2;
104+
102105
export const finalizeConfigurationHints = (
103106
results: Results,
104107
options: { cwd: string; configFilePath?: string }
105108
): ProcessedHint[] => {
106-
if (results.counters.files > 20) {
109+
const { files, processed } = results.counters;
110+
const unusedFileRatio = processed > 0 ? files / processed : 0;
111+
if (files > UNCONFIGURED_MIN_FILES && unusedFileRatio > UNCONFIGURED_MIN_RATIO) {
107112
const workspaces = results.includedWorkspaceDirs
108113
.sort(byPathDepth)
109114
.reverse()

packages/mcp-server/src/curated-resources.js

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,36 @@
1+
/** @type {Record<string, { name: string; description: string; path: string; featured?: boolean }>} */
12
export const CURATED_RESOURCES = {
2-
'getting-started': {
3-
name: 'Getting Started',
4-
description: 'New to Knip? Start here for installation and first run',
5-
path: 'overview/getting-started.mdx',
6-
},
73
configuration: {
84
name: 'Configuration',
95
description: 'Understand configuration basics, defaults, and file locations',
106
path: 'overview/configuration.md',
117
},
8+
'configuration-hints': {
9+
name: 'Configuration Hints',
10+
description: 'Decode the configurationHints from knip-run: each hint type and how to resolve it',
11+
path: 'reference/configuration-hints.md',
12+
},
1213
'configuring-project-files': {
1314
name: 'Configuring Project Files',
1415
description: 'READ FIRST for unused files or false positives. Covers entry/project patterns',
1516
path: 'guides/configuring-project-files.md',
1617
},
1718
'handling-issues': {
1819
name: 'Handling Issues',
19-
description: 'How to handle each issue type: files, dependencies, exports, types, duplicates',
20+
description: 'How to resolve each issue type: files, dependencies, exports, types, duplicates',
2021
path: 'guides/handling-issues.mdx',
2122
},
23+
'known-issues': {
24+
name: 'Known Issues',
25+
description: 'Errors or unexpected behavior? Check workarounds for common problems',
26+
path: 'reference/known-issues.md',
27+
},
28+
'issue-types': {
29+
name: 'Issue Types',
30+
description:
31+
'Decode knip-run output: every issue type, what it means and its key (files, dependencies, exports, …)',
32+
path: 'reference/issue-types.md',
33+
},
2234
'monorepos-and-workspaces': {
2335
name: 'Monorepos & Workspaces',
2436
description: 'Multi-package repo? Configure workspaces and cross-references here',
@@ -29,11 +41,21 @@ export const CURATED_RESOURCES = {
2941
description: 'Exclude tests, stories, devDependencies with --production and --strict flags',
3042
path: 'features/production-mode.md',
3143
},
44+
'rules-and-filters': {
45+
name: 'Rules & Filters',
46+
description: 'Focus or mute issue types with --include/--exclude and rules',
47+
path: 'features/rules-and-filters.md',
48+
},
3249
compilers: {
3350
name: 'Compilers',
3451
description: 'Using .vue, .svelte, .astro, .mdx files? Configure compilers to parse them',
3552
path: 'features/compilers.md',
3653
},
54+
'jsdoc-tsdoc-tags': {
55+
name: 'JSDoc & TSDoc Tags',
56+
description: 'Keep an export but exclude it from the report with @public/@internal and custom tags',
57+
path: 'reference/jsdoc-tsdoc-tags.md',
58+
},
3759
'configuration-reference': {
3860
name: 'Configuration Reference',
3961
description: 'Complete reference of all config options: entry, project, ignore, plugins, etc.',
@@ -54,9 +76,4 @@ export const CURATED_RESOURCES = {
5476
description: 'Check if a plugin exists for your tool (Jest, Vitest, ESLint, etc.)',
5577
path: 'reference/plugins.md',
5678
},
57-
'known-issues': {
58-
name: 'Known Issues',
59-
description: 'Errors or unexpected behavior? Check workarounds for common problems',
60-
path: 'reference/known-issues.md',
61-
},
6279
};

packages/mcp-server/src/texts.js

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,52 +3,57 @@ import { CURATED_RESOURCES } from './curated-resources.js';
33
export const WORKFLOW = `Workflow:
44
55
1. Read essential documentation resources:
6+
- how-knip-works (the mental model: entry files, module graph, reachability)
67
- configuring-project-files (must read to configure entry patterns)
78
- plugins-explanation (understand entries added by auto-detected plugins)
8-
- handling-issues (comprehensive guide to deal with any reported issue type)
9+
- handling-issues (comprehensive guide to resolve any reported issue type)
910
- configuration-reference (all knip.json configuration options)
10-
2. Run analysis (knip-run) to get configuration hints and issues
11-
3. Address the hints by adjusting knip.json
12-
4. Repeat steps 2-3 until hints are gone and false positives are minimized
11+
2. Run analysis (knip-run): returns configurationHints (decode with configuration-hints) and issues keyed by type (decode with issue-types)
12+
3. Address the configuration hints by adjusting knip.json
13+
4. Repeat steps 2-3 until configuration hints are gone and remaining issues are genuine findings
14+
15+
You're done when there are no configuration hints and the issues are genuine (or zero). Many projects need NO knip.json: Knip auto-detects plugins, so configFile exists:false with zero hints is a good, complete result. Don't add configuration just to have it.
1316
1417
Important notes:
1518
- For prompts like "run knip" or "clean up codebase" or "no more slop": always run workflow to configure Knip first.
1619
- If you hit errors (not lint issues), consult: known-issues
17-
- Before suggesting fixes/solutions, consult: handling-issues and reference/jsdoc-tsdoc-tags
18-
- For cleanup, consult: features/auto-fix
19-
- To install Knip and start using it from CLI, consult: getting-started and reference/cli
20+
- Before suggesting fixes/solutions, consult: handling-issues and jsdoc-tsdoc-tags
21+
- Prefer specific solutions over "ignore"; to focus or mute issue types, consult: rules-and-filters
22+
- For cleanup, consult: auto-fix
23+
- To install Knip and start using it from CLI, consult: getting-started and cli
2024
- Knip does not remove unused imports/variables inside files (use a linter)
2125
`;
2226

2327
// pkg.contributes.languageModelTools[0].modelDescription
2428
export const RUN_KNIP_TOOL_DESCRIPTION = `Run Knip and return configuration hints and issues.
2529
2630
Returns:
31+
- totalIssues: Total number of issues found (0 = clean)
2732
- configurationHints: Ordered suggestions to improve configuration (address these first)
28-
- counters: Summary counts of each issue type
33+
- counters: Per-issue-type totals
34+
- maybeUnconfigured: true when a high unused-file count suggests entry/project may be incomplete (review configurationHints)
35+
- truncated: true when the sample was capped; read counters for totals, or scope with workspace to see the rest
2936
- enabledPlugins: Auto-detected plugins per workspace
30-
- files: List of unused files
31-
- issues: Detailed issues by type (dependencies, exports, types, etc.)
32-
- configFile: Current config file status
37+
- files: Unused files (sample, might be capped)
38+
- issues: Issues per type, i.e. dependencies, exports, types, etc. (sample, capped per type)
39+
- configFile: Config file status (exists:false is normal when plugins auto-configure)
3340
3441
Options:
3542
- cwd: Working directory (defaults to the process cwd)
36-
- workspace: Scope run to array of workspaces
43+
- workspace: Scope run to array of workspaces (to drill into a package when truncated)
3744
3845
Iterate: adjust knip.json based on hints, run again until no hints remain and false positives are minimized.`;
3946

4047
// pkg.contributes.languageModelTools[1].modelDescription
41-
export const DOC_TOOL_DESCRIPTION = `Get Knip documentation by topic.
42-
43-
If registered resources are unavailable, use this tool.
48+
export const DOC_TOOL_DESCRIPTION = `Fetch a Knip doc by topic id, or by any docs path. Consult it to resolve a finding, hint, or error instead of guessing.
4449
4550
Available topics (use these IDs):
4651
${Object.entries(CURATED_RESOURCES)
4752
.map(([id, doc]) => `- ${id}: ${doc.description}`)
4853
.join('\n')}
54+
}
4955
50-
Can also fetch any doc by path (e.g. "reference/cli" or "guides/troubleshooting").
51-
Use this instead of fetching from https://knip.dev.`;
56+
Full descriptions are registered as MCP resources; any page is also reachable by path (e.g. "guides/troubleshooting"). Use this instead of fetching from https://knip.dev.`;
5257

5358
// pkg.contributes.languageModelTools[1].inputSchema.properties.topic.description
5459
export const DOC_TOOL_TOPIC_DESCRIPTION =
@@ -61,15 +66,18 @@ export const ERROR_HINT = `For unexpected errors (exit code 2) such as "Error lo
6166
- Apply a workaround, then run knip-run again`;
6267

6368
export const UNCONFIGURED_HINT =
64-
'Issues are suppressed because the project is not yet configured. Reported issues might be false positives. Address configuration hints first, then re-run to get the actual issues.';
69+
'Many unused files were found. This often means entry/project patterns are not configured yet (so some results may be false positives), but could mean real dead code. A capped sample of issues is shown — see `counters` for true totals. Address the configurationHints first, then re-run; or scope with `workspace` to analyze one package at a time.';
6570

66-
export const CONFIG_REVIEW_HINT = `Review the existing configuration for potential improvements:
71+
export const CONFIG_REVIEW_HINT = `Review the configuration for improvements that apply to the issues you see (skip the ones that don't):
6772
68-
- Never use "ignore" patterns (hides real issues!), always prefer specific solutions; other ignore* options are allowed
73+
- Avoid the broad "ignore" option (file globs); it hides real issues, so prefer specific fixes. Targeted ignoreDependencies/ignoreBinaries/ignoreUnresolved are fine for justified, known false positives
6974
- Many unused exported types → try ignoreExportsUsedInFile: { interface: true, type: true }
7075
- Remove ignore patterns that don't match any files
7176
- Remove redundant ignore patterns — Knip respects .gitignore (node_modules, dist, build, .git)
7277
- Remove entry patterns covered by config defaults and auto-detected plugins
7378
- Config files (e.g. vite.config.ts) showing as unused → try enable or disable the plugin explicitly (vite: true)
7479
- Dependencies matching Node.js builtins: add to ignoreDependencies (e.g. buffer, process)
7580
- Unresolved imports from path aliases: add paths to Knip config (tsconfig.json semantics)`;
81+
82+
export const CLEAN_HINT =
83+
'No issues and no configuration hints: Knip is happy. Many projects need no knip.json (plugins are auto-detected), so this is a complete, successful result with nothing more to configure.';

packages/mcp-server/src/tools.js

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { dirname, join } from 'node:path';
33
import { fileURLToPath } from 'node:url';
44
import { createOptions, createSession, finalizeConfigurationHints, KNIP_CONFIG_LOCATIONS } from 'knip/session';
55
import { CURATED_RESOURCES } from './curated-resources.js';
6-
import { CONFIG_REVIEW_HINT, UNCONFIGURED_HINT } from './texts.js';
6+
import { CLEAN_HINT, CONFIG_REVIEW_HINT, UNCONFIGURED_HINT } from './texts.js';
7+
import { transformForAgent } from './transform.js';
78

89
export { ERROR_HINT } from './texts.js';
910

@@ -25,32 +26,70 @@ export function getErrorMessage(error) {
2526
const __dirname = dirname(fileURLToPath(import.meta.url));
2627
const docsDir = join(__dirname, './docs');
2728

29+
const MAX_UNUSED_FILES = 50;
30+
const MAX_ISSUES_PER_TYPE = 50;
31+
2832
/**
2933
* @param {import('knip/session').Results} results
3034
* @param {{ cwd: string, configFilePath: string | undefined }} options
3135
*/
3236
export function buildResults(results, options) {
3337
const configurationHints = finalizeConfigurationHints(results, options);
3438

35-
const isSuppressIssues =
36-
results.counters.total >= 10 &&
37-
configurationHints.some(hint => hint.type === 'top-level-unconfigured' || hint.type === 'workspace-unconfigured');
39+
const maybeUnconfigured = configurationHints.some(
40+
hint => hint.type === 'top-level-unconfigured' || hint.type === 'workspace-unconfigured'
41+
);
3842

3943
const configFile = options.configFilePath
4044
? { exists: true, filePath: options.configFilePath }
4145
: { exists: false, locations: KNIP_CONFIG_LOCATIONS };
4246

43-
const reviewHint = isSuppressIssues ? UNCONFIGURED_HINT : options.configFilePath ? CONFIG_REVIEW_HINT : undefined;
44-
const files = isSuppressIssues ? null : Object.keys(results.issues.files);
45-
const issues = isSuppressIssues
46-
? null
47-
: Object.fromEntries(Object.entries(results.issues).filter(([key]) => key !== 'files' && key !== '_files'));
47+
const counters = Object.fromEntries(
48+
Object.entries(results.counters).filter(([key]) => key !== 'processed' && key !== 'total')
49+
);
50+
51+
const totalIssues = Object.values(counters).reduce((sum, count) => sum + count, 0);
52+
const isClean = totalIssues === 0 && configurationHints.length === 0;
53+
const reviewHint = maybeUnconfigured
54+
? UNCONFIGURED_HINT
55+
: isClean
56+
? CLEAN_HINT
57+
: options.configFilePath && totalIssues > 0
58+
? CONFIG_REVIEW_HINT
59+
: undefined;
60+
61+
let truncated = false;
62+
63+
const unusedFiles = Object.keys(results.issues.files);
64+
const files = unusedFiles.slice(0, MAX_UNUSED_FILES);
65+
if (unusedFiles.length > files.length) truncated = true;
66+
67+
const issues = {};
68+
for (const [type, byFile] of Object.entries(results.issues)) {
69+
if (type === 'files' || type === '_files') continue;
70+
const items = [];
71+
for (const [file, symbols] of Object.entries(byFile)) {
72+
for (const issue of Object.values(symbols)) {
73+
items.push({
74+
file,
75+
symbol: issue.symbol,
76+
line: issue.line ?? issue.symbols?.[0]?.line,
77+
...(issue.parentSymbol ? { parent: issue.parentSymbol } : {}),
78+
});
79+
}
80+
}
81+
if (items.length === 0) continue;
82+
if (items.length > MAX_ISSUES_PER_TYPE) truncated = true;
83+
issues[type] = items.slice(0, MAX_ISSUES_PER_TYPE);
84+
}
4885

4986
return {
5087
reviewHint,
51-
issuesSuppressed: isSuppressIssues,
88+
maybeUnconfigured,
89+
truncated,
5290
configurationHints,
53-
counters: results.counters,
91+
counters,
92+
totalIssues,
5493
configFile,
5594
enabledPlugins: results.enabledPlugins,
5695
files,
@@ -72,7 +111,7 @@ export async function getResults(cwd, workspace) {
72111
export function readContent(filePath) {
73112
try {
74113
const content = readFileSync(join(docsDir, filePath), 'utf-8');
75-
return content.replace(/^---[\s\S]*?---\n/, '');
114+
return transformForAgent(content, filePath);
76115
} catch (error) {
77116
return `Error reading ${filePath}: ${error instanceof Error ? error.message : String(error)}`;
78117
}
@@ -90,9 +129,7 @@ export function getDocs(topic) {
90129

91130
/** @param {string} topic */
92131
function findDocPage(topic) {
93-
/** @type {Record<string, { name: string; description: string; path: string }>} */
94-
const resources = CURATED_RESOURCES;
95-
if (resources[topic]) return readContent(resources[topic].path);
132+
if (CURATED_RESOURCES[topic]) return readContent(CURATED_RESOURCES[topic].path);
96133

97134
for (const ext of ['.md', '.mdx']) {
98135
const filePath = join(docsDir, `${topic}${ext}`);
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { posix } from 'node:path';
2+
3+
const DOC_LINK = /\.(md|mdx)(#.*)?$/;
4+
const ASIDE = /^:::(tip|note|caution|danger|warning)(?:\[([^\]]*)\])?\s*$/;
5+
const FENCE = /^(\s*)(`{3,}|~{3,})/;
6+
const ESM = /^(import|export)\b/;
7+
const TAB_ITEM = /^\s*<TabItem\s+[^>]*label=["']([^"']+)["'][^>]*>\s*$/;
8+
const TAG_LINE = /^\s*<\/?[A-Za-z][\w.-]*(?:\s[^<>]*)?\/?>\s*$/;
9+
10+
/**
11+
* Rewrite a relative doc link into a knip-docs topic path, so an agent can pass
12+
* it straight back to the `knip-docs` tool. Leaves external/anchor/site links as-is.
13+
* e.g. `../reference/integrations.md#filters` on a `guides/` page → `reference/integrations#filters`
14+
* @param {string} target
15+
* @param {string} dir
16+
* @returns {string}
17+
*/
18+
function rewriteLink(target, dir) {
19+
if (/^(https?:|mailto:|tel:|#|\/)/.test(target)) return target;
20+
if (!DOC_LINK.test(target)) return target;
21+
const [path, anchor] = target.split('#');
22+
const resolved = posix.normalize(posix.join(dir, path)).replace(/\.(md|mdx)$/, '');
23+
return anchor ? `${resolved}#${anchor}` : resolved;
24+
}
25+
26+
/**
27+
* Make a doc page self-contained for an agent reading it cold via MCP: strip
28+
* frontmatter and MDX import/export statements, flatten `<Tabs>`/`<TabItem>` and
29+
* Starlight asides to plain markdown, drop remaining standalone JSX/HTML tags,
30+
* and rewrite relative doc links to `knip-docs` topic paths. Code fences are left
31+
* untouched (incl. nested longer fences).
32+
* @param {string} content
33+
* @param {string} filePath relative path under the docs dir, e.g. `guides/handling-issues.mdx`
34+
* @returns {string}
35+
*/
36+
export function transformForAgent(content, filePath) {
37+
const dir = posix.dirname(filePath);
38+
const body = content.replace(/^---[\s\S]*?---\n/, '');
39+
const out = [];
40+
let fence = null;
41+
let inEsm = false;
42+
let esmDepth = 0;
43+
44+
for (const line of body.split('\n')) {
45+
const f = line.match(FENCE);
46+
if (f) {
47+
const marker = f[2];
48+
if (!fence) fence = marker;
49+
else if (marker[0] === fence[0] && marker.length >= fence.length) fence = null;
50+
out.push(line);
51+
continue;
52+
}
53+
if (fence) {
54+
out.push(line);
55+
continue;
56+
}
57+
58+
// Drop MDX import/export statements, balancing braces to span multi-line ones
59+
if (!inEsm && ESM.test(line)) inEsm = true;
60+
if (inEsm) {
61+
for (const ch of line) {
62+
if (ch === '{') esmDepth++;
63+
else if (ch === '}') esmDepth--;
64+
}
65+
if (esmDepth <= 0) {
66+
inEsm = false;
67+
esmDepth = 0;
68+
}
69+
continue;
70+
}
71+
72+
const tab = line.match(TAB_ITEM);
73+
const aside = line.match(ASIDE);
74+
if (tab) {
75+
out.push(`**${tab[1]}**`);
76+
} else if (aside) {
77+
out.push(`**${aside[2] || aside[1][0].toUpperCase() + aside[1].slice(1)}**`);
78+
} else if (/^:::\s*$/.test(line) || TAG_LINE.test(line)) {
79+
// drop aside close and any standalone tag (<Tabs>, </TabItem>, <Card>, <video>, <source/>, …)
80+
} else {
81+
out.push(
82+
line
83+
.replace(/\]\(([^)\s]+)(\s+"[^"]*")?\)/g, (_m, t, title) => `](${rewriteLink(t, dir)}${title || ''})`)
84+
.replace(/^(\[[^\]]+\]:\s+)(\S+)(.*)$/, (_m, pre, t, rest) => `${pre}${rewriteLink(t, dir)}${rest}`)
85+
);
86+
}
87+
}
88+
89+
return out.join('\n');
90+
}

0 commit comments

Comments
 (0)