mdcontext 0.1.0 → 0.2.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/.changeset/config.json +9 -9
- package/.claude/settings.local.json +25 -0
- package/.github/workflows/claude-code-review.yml +44 -0
- package/.github/workflows/claude.yml +85 -0
- package/CONTRIBUTING.md +186 -0
- package/NOTES/NOTES +44 -0
- package/README.md +206 -3
- package/biome.json +1 -1
- package/dist/chunk-23UPXDNL.js +3044 -0
- package/dist/chunk-2W7MO2DL.js +1366 -0
- package/dist/chunk-3NUAZGMA.js +1689 -0
- package/dist/chunk-7TOWB2XB.js +366 -0
- package/dist/chunk-7XOTOADQ.js +3065 -0
- package/dist/chunk-AH2PDM2K.js +3042 -0
- package/dist/chunk-BNXWSZ63.js +3742 -0
- package/dist/chunk-BTL5DJVU.js +3222 -0
- package/dist/chunk-HDHYG7E4.js +104 -0
- package/dist/chunk-HLR4KZBP.js +3234 -0
- package/dist/chunk-IP3FRFEB.js +1045 -0
- package/dist/chunk-KHU56VDO.js +3042 -0
- package/dist/chunk-KRYIFLQR.js +85 -89
- package/dist/chunk-LBSDNLEM.js +287 -0
- package/dist/chunk-MNTQ7HCP.js +2643 -0
- package/dist/chunk-MUJELQQ6.js +1387 -0
- package/dist/chunk-MXJGMSLV.js +2199 -0
- package/dist/chunk-N6QJGC3Z.js +2636 -0
- package/dist/chunk-OBELGBPM.js +1713 -0
- package/dist/chunk-OT7R5XTA.js +3192 -0
- package/dist/chunk-P7X4RA2T.js +106 -0
- package/dist/chunk-PIDUQNC2.js +3185 -0
- package/dist/chunk-POGCDIH4.js +3187 -0
- package/dist/chunk-PSIEOQGZ.js +3043 -0
- package/dist/chunk-PVRT3IHA.js +3238 -0
- package/dist/chunk-QNN4TT23.js +1430 -0
- package/dist/chunk-RE3R45RJ.js +3042 -0
- package/dist/chunk-S7E6TFX6.js +718 -657
- package/dist/chunk-SG6GLU4U.js +1378 -0
- package/dist/chunk-SJCDV2ST.js +274 -0
- package/dist/chunk-SYE5XLF3.js +104 -0
- package/dist/chunk-T5VLYBZD.js +103 -0
- package/dist/chunk-TOQB7VWU.js +3238 -0
- package/dist/chunk-VFNMZ4ZQ.js +3228 -0
- package/dist/chunk-VVTGZNBT.js +1533 -1423
- package/dist/chunk-W7Q4RFEV.js +104 -0
- package/dist/chunk-XTYYVRLO.js +3190 -0
- package/dist/chunk-Y6MDYVJD.js +3063 -0
- package/dist/cli/main.js +4072 -629
- package/dist/index.d.ts +420 -33
- package/dist/index.js +8 -15
- package/dist/mcp/server.js +103 -7
- package/dist/schema-BAWSG7KY.js +22 -0
- package/dist/schema-E3QUPL26.js +20 -0
- package/dist/schema-EHL7WUT6.js +20 -0
- package/docs/019-USAGE.md +44 -5
- package/docs/020-current-implementation.md +8 -8
- package/docs/021-DOGFOODING-FINDINGS.md +1 -1
- package/docs/CONFIG.md +1123 -0
- package/docs/ERRORS.md +383 -0
- package/docs/summarization.md +320 -0
- package/justfile +40 -0
- package/package.json +39 -33
- package/research/INDEX.md +315 -0
- package/research/code-review/README.md +90 -0
- package/research/code-review/cli-error-handling-review.md +979 -0
- package/research/code-review/code-review-validation-report.md +464 -0
- package/research/code-review/main-ts-review.md +1128 -0
- package/research/config-docs/SUMMARY.md +357 -0
- package/research/config-docs/TEST-RESULTS.md +776 -0
- package/research/config-docs/TODO.md +542 -0
- package/research/config-docs/analysis.md +744 -0
- package/research/config-docs/fix-validation.md +502 -0
- package/research/config-docs/help-audit.md +264 -0
- package/research/config-docs/help-system-analysis.md +890 -0
- package/research/frontmatter/COMMENTS-ARE-SKIPPED.md +149 -0
- package/research/frontmatter/LLM-CODE-NAVIGATION.md +276 -0
- package/research/issue-review.md +603 -0
- package/research/llm-summarization/agent-cli-tools-2026.md +1082 -0
- package/research/llm-summarization/alternative-providers-2026.md +1428 -0
- package/research/llm-summarization/anthropic-2026.md +367 -0
- package/research/llm-summarization/claude-cli-integration.md +1706 -0
- package/research/llm-summarization/cli-integration-patterns.md +3155 -0
- package/research/llm-summarization/openai-2026.md +473 -0
- package/research/llm-summarization/openai-compatible-providers-2026.md +1022 -0
- package/research/llm-summarization/opencode-cli-integration.md +1552 -0
- package/research/llm-summarization/prompt-engineering-2026.md +1426 -0
- package/research/llm-summarization/prototype-results.md +56 -0
- package/research/llm-summarization/provider-switching-patterns-2026.md +2153 -0
- package/research/llm-summarization/typescript-llm-libraries-2026.md +2436 -0
- package/research/mdcontext-pudding/00-EXECUTIVE-SUMMARY.md +282 -0
- package/research/mdcontext-pudding/01-index-embed.md +956 -0
- package/research/mdcontext-pudding/02-search-COMMANDS.md +142 -0
- package/research/mdcontext-pudding/02-search-SUMMARY.md +146 -0
- package/research/mdcontext-pudding/02-search.md +970 -0
- package/research/mdcontext-pudding/03-context.md +779 -0
- package/research/mdcontext-pudding/04-navigation-and-analytics.md +803 -0
- package/research/mdcontext-pudding/04-tree.md +704 -0
- package/research/mdcontext-pudding/05-config.md +1038 -0
- package/research/mdcontext-pudding/06-links-summary.txt +87 -0
- package/research/mdcontext-pudding/06-links.md +679 -0
- package/research/mdcontext-pudding/07-stats.md +693 -0
- package/research/mdcontext-pudding/BUG-FIX-PLAN.md +388 -0
- package/research/mdcontext-pudding/P0-BUG-VALIDATION.md +167 -0
- package/research/mdcontext-pudding/README.md +168 -0
- package/research/mdcontext-pudding/TESTING-SUMMARY.md +128 -0
- package/research/research-quality-review.md +834 -0
- package/research/semantic-search/embedding-text-analysis.md +156 -0
- package/research/semantic-search/multi-word-failure-reproduction.md +171 -0
- package/research/semantic-search/query-processing-analysis.md +207 -0
- package/research/semantic-search/root-cause-and-solution.md +114 -0
- package/research/semantic-search/threshold-validation-report.md +69 -0
- package/research/semantic-search/vector-search-analysis.md +63 -0
- package/research/test-path-issues.md +276 -0
- package/review/ALP-76/1-error-type-design.md +962 -0
- package/review/ALP-76/2-error-handling-patterns.md +906 -0
- package/review/ALP-76/3-error-presentation.md +624 -0
- package/review/ALP-76/4-test-coverage.md +625 -0
- package/review/ALP-76/5-migration-completeness.md +440 -0
- package/review/ALP-76/6-effect-best-practices.md +755 -0
- package/scripts/apply-branch-protection.sh +47 -0
- package/scripts/branch-protection-templates.json +79 -0
- package/scripts/prototype-summarization.ts +346 -0
- package/scripts/rebuild-hnswlib.js +32 -37
- package/scripts/setup-branch-protection.sh +64 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/active-provider.json +7 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.json +541 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.meta.json +5 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/config.json +8 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/documents.json +60 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/links.json +13 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/sections.json +1197 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/configuration-management.md +99 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/distributed-systems.md +92 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/error-handling.md +78 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/failure-automation.md +55 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/job-context.md +69 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/process-orchestration.md +99 -0
- package/src/cli/argv-preprocessor.test.ts +2 -2
- package/src/cli/cli.test.ts +230 -33
- package/src/cli/commands/config-cmd.ts +642 -0
- package/src/cli/commands/context.ts +97 -9
- package/src/cli/commands/duplicates.ts +122 -0
- package/src/cli/commands/embeddings.ts +529 -0
- package/src/cli/commands/index-cmd.ts +210 -30
- package/src/cli/commands/index.ts +3 -0
- package/src/cli/commands/search.ts +894 -64
- package/src/cli/commands/stats.ts +3 -0
- package/src/cli/commands/tree.ts +26 -5
- package/src/cli/config-layer.ts +176 -0
- package/src/cli/error-handler.test.ts +235 -0
- package/src/cli/error-handler.ts +655 -0
- package/src/cli/flag-schemas.ts +66 -0
- package/src/cli/help.ts +209 -7
- package/src/cli/main.ts +348 -58
- package/src/cli/options.ts +10 -0
- package/src/cli/shared-error-handling.ts +199 -0
- package/src/cli/utils.ts +150 -17
- package/src/config/file-provider.test.ts +320 -0
- package/src/config/file-provider.ts +273 -0
- package/src/config/index.ts +72 -0
- package/src/config/integration.test.ts +667 -0
- package/src/config/precedence.test.ts +277 -0
- package/src/config/precedence.ts +451 -0
- package/src/config/schema.test.ts +414 -0
- package/src/config/schema.ts +603 -0
- package/src/config/service.test.ts +320 -0
- package/src/config/service.ts +243 -0
- package/src/config/testing.test.ts +264 -0
- package/src/config/testing.ts +110 -0
- package/src/core/types.ts +6 -33
- package/src/duplicates/detector.test.ts +183 -0
- package/src/duplicates/detector.ts +414 -0
- package/src/duplicates/index.ts +18 -0
- package/src/embeddings/embedding-namespace.test.ts +300 -0
- package/src/embeddings/embedding-namespace.ts +947 -0
- package/src/embeddings/heading-boost.test.ts +222 -0
- package/src/embeddings/hnsw-build-options.test.ts +198 -0
- package/src/embeddings/hyde.test.ts +272 -0
- package/src/embeddings/hyde.ts +264 -0
- package/src/embeddings/index.ts +2 -0
- package/src/embeddings/openai-provider.ts +332 -83
- package/src/embeddings/pricing.json +22 -0
- package/src/embeddings/provider-constants.ts +204 -0
- package/src/embeddings/provider-errors.test.ts +967 -0
- package/src/embeddings/provider-errors.ts +565 -0
- package/src/embeddings/provider-factory.test.ts +240 -0
- package/src/embeddings/provider-factory.ts +225 -0
- package/src/embeddings/provider-integration.test.ts +788 -0
- package/src/embeddings/query-preprocessing.test.ts +187 -0
- package/src/embeddings/semantic-search-threshold.test.ts +508 -0
- package/src/embeddings/semantic-search.ts +780 -93
- package/src/embeddings/types.ts +293 -16
- package/src/embeddings/vector-store.ts +486 -77
- package/src/embeddings/voyage-provider.ts +313 -0
- package/src/errors/errors.test.ts +845 -0
- package/src/errors/index.ts +533 -0
- package/src/index/ignore-patterns.test.ts +354 -0
- package/src/index/ignore-patterns.ts +305 -0
- package/src/index/indexer.ts +286 -48
- package/src/index/storage.ts +94 -30
- package/src/index/types.ts +40 -2
- package/src/index/watcher.ts +67 -9
- package/src/index.ts +22 -0
- package/src/integration/search-keyword.test.ts +678 -0
- package/src/mcp/server.ts +135 -6
- package/src/parser/parser.ts +18 -19
- package/src/parser/section-filter.test.ts +277 -0
- package/src/parser/section-filter.ts +125 -3
- package/src/search/__tests__/hybrid-search.test.ts +650 -0
- package/src/search/bm25-store.ts +366 -0
- package/src/search/cross-encoder.test.ts +253 -0
- package/src/search/cross-encoder.ts +406 -0
- package/src/search/fuzzy-search.test.ts +419 -0
- package/src/search/fuzzy-search.ts +273 -0
- package/src/search/hybrid-search.ts +448 -0
- package/src/search/path-matcher.test.ts +276 -0
- package/src/search/path-matcher.ts +33 -0
- package/src/search/searcher.test.ts +99 -1
- package/src/search/searcher.ts +189 -67
- package/src/search/wink-bm25.d.ts +30 -0
- package/src/summarization/cli-providers/claude.ts +202 -0
- package/src/summarization/cli-providers/detection.test.ts +273 -0
- package/src/summarization/cli-providers/detection.ts +118 -0
- package/src/summarization/cli-providers/index.ts +8 -0
- package/src/summarization/cost.test.ts +139 -0
- package/src/summarization/cost.ts +102 -0
- package/src/summarization/error-handler.test.ts +127 -0
- package/src/summarization/error-handler.ts +111 -0
- package/src/summarization/index.ts +102 -0
- package/src/summarization/pipeline.test.ts +498 -0
- package/src/summarization/pipeline.ts +231 -0
- package/src/summarization/prompts.test.ts +269 -0
- package/src/summarization/prompts.ts +133 -0
- package/src/summarization/provider-factory.test.ts +396 -0
- package/src/summarization/provider-factory.ts +178 -0
- package/src/summarization/types.ts +184 -0
- package/src/summarize/summarizer.ts +104 -35
- package/src/types/huggingface-transformers.d.ts +66 -0
- package/tests/fixtures/cli/.mdcontext/active-provider.json +7 -0
- package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/indexes/documents.json +4 -4
- package/tests/fixtures/cli/.mdcontext/indexes/sections.json +14 -0
- package/tests/integration/embed-index.test.ts +712 -0
- package/tests/integration/search-context.test.ts +469 -0
- package/tests/integration/search-semantic.test.ts +522 -0
- package/vitest.config.ts +1 -6
- package/AGENTS.md +0 -46
- package/tests/fixtures/cli/.mdcontext/vectors.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/vectors.meta.json +0 -1264
package/src/mcp/server.ts
CHANGED
|
@@ -6,7 +6,14 @@
|
|
|
6
6
|
* Exposes markdown analysis tools for Claude integration
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { createRequire } from 'node:module'
|
|
9
10
|
import * as path from 'node:path'
|
|
11
|
+
|
|
12
|
+
// Read version from package.json using createRequire for ESM compatibility
|
|
13
|
+
const require = createRequire(import.meta.url)
|
|
14
|
+
const packageJson = require('../../package.json') as { version: string }
|
|
15
|
+
const MCP_VERSION: string = packageJson.version
|
|
16
|
+
|
|
10
17
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
11
18
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
12
19
|
import type { CallToolResult, Tool } from '@modelcontextprotocol/sdk/types.js'
|
|
@@ -17,7 +24,11 @@ import {
|
|
|
17
24
|
import { Effect } from 'effect'
|
|
18
25
|
import type { MdSection } from '../core/types.js'
|
|
19
26
|
import { semanticSearch } from '../embeddings/semantic-search.js'
|
|
20
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
buildIndex,
|
|
29
|
+
getIncomingLinks,
|
|
30
|
+
getOutgoingLinks,
|
|
31
|
+
} from '../index/indexer.js'
|
|
21
32
|
import { parseFile } from '../parser/parser.js'
|
|
22
33
|
import { search } from '../search/searcher.js'
|
|
23
34
|
import { formatSummary, summarizeFile } from '../summarize/summarizer.js'
|
|
@@ -52,8 +63,8 @@ const tools: Tool[] = [
|
|
|
52
63
|
},
|
|
53
64
|
threshold: {
|
|
54
65
|
type: 'number',
|
|
55
|
-
description: 'Minimum similarity threshold 0-1 (default: 0.
|
|
56
|
-
default: 0.
|
|
66
|
+
description: 'Minimum similarity threshold 0-1 (default: 0.35)',
|
|
67
|
+
default: 0.35,
|
|
57
68
|
},
|
|
58
69
|
},
|
|
59
70
|
required: ['query'],
|
|
@@ -154,6 +165,36 @@ const tools: Tool[] = [
|
|
|
154
165
|
},
|
|
155
166
|
},
|
|
156
167
|
},
|
|
168
|
+
{
|
|
169
|
+
name: 'md_links',
|
|
170
|
+
description:
|
|
171
|
+
'Get outgoing links from a markdown file. Shows what files this document references/links to.',
|
|
172
|
+
inputSchema: {
|
|
173
|
+
type: 'object',
|
|
174
|
+
properties: {
|
|
175
|
+
path: {
|
|
176
|
+
type: 'string',
|
|
177
|
+
description: 'Path to the markdown file',
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
required: ['path'],
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'md_backlinks',
|
|
185
|
+
description:
|
|
186
|
+
'Get incoming links to a markdown file. Shows what files reference/link to this document.',
|
|
187
|
+
inputSchema: {
|
|
188
|
+
type: 'object',
|
|
189
|
+
properties: {
|
|
190
|
+
path: {
|
|
191
|
+
type: 'string',
|
|
192
|
+
description: 'Path to the markdown file',
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
required: ['path'],
|
|
196
|
+
},
|
|
197
|
+
},
|
|
157
198
|
]
|
|
158
199
|
|
|
159
200
|
// ============================================================================
|
|
@@ -167,8 +208,11 @@ const handleMdSearch = async (
|
|
|
167
208
|
const query = args.query as string
|
|
168
209
|
const limit = (args.limit as number) ?? 5
|
|
169
210
|
const pathFilter = args.path_filter as string | undefined
|
|
170
|
-
const threshold = (args.threshold as number) ?? 0.
|
|
211
|
+
const threshold = (args.threshold as number) ?? 0.35
|
|
171
212
|
|
|
213
|
+
// Note: catchAll is intentional at this MCP boundary layer.
|
|
214
|
+
// MCP protocol requires JSON error responses, so we convert typed errors
|
|
215
|
+
// to { error: message } format for protocol compliance.
|
|
172
216
|
const result = await Effect.runPromise(
|
|
173
217
|
semanticSearch(rootPath, query, {
|
|
174
218
|
limit,
|
|
@@ -228,6 +272,7 @@ const handleMdContext = async (
|
|
|
228
272
|
? filePath
|
|
229
273
|
: path.join(rootPath, filePath)
|
|
230
274
|
|
|
275
|
+
// Note: catchAll is intentional - MCP boundary converts errors to JSON format
|
|
231
276
|
const result = await Effect.runPromise(
|
|
232
277
|
summarizeFile(resolvedPath, { level, maxTokens }).pipe(
|
|
233
278
|
Effect.catchAll((e) => Effect.succeed({ error: e.message })),
|
|
@@ -255,9 +300,9 @@ const handleMdStructure = async (
|
|
|
255
300
|
? filePath
|
|
256
301
|
: path.join(rootPath, filePath)
|
|
257
302
|
|
|
303
|
+
// Note: catchAll is intentional - MCP boundary converts errors to JSON format
|
|
258
304
|
const result = await Effect.runPromise(
|
|
259
305
|
parseFile(resolvedPath).pipe(
|
|
260
|
-
Effect.mapError((e) => new Error(`${e._tag}: ${e.message}`)),
|
|
261
306
|
Effect.catchAll((e) => Effect.succeed({ error: e.message })),
|
|
262
307
|
),
|
|
263
308
|
)
|
|
@@ -310,6 +355,7 @@ const handleMdKeywordSearch = async (
|
|
|
310
355
|
const hasTable = args.has_table as boolean | undefined
|
|
311
356
|
const limit = (args.limit as number) ?? 20
|
|
312
357
|
|
|
358
|
+
// Note: catchAll is intentional - MCP boundary converts errors to JSON format
|
|
313
359
|
const result = await Effect.runPromise(
|
|
314
360
|
search(rootPath, {
|
|
315
361
|
heading,
|
|
@@ -381,6 +427,7 @@ const handleMdIndex = async (
|
|
|
381
427
|
? indexPath
|
|
382
428
|
: path.join(rootPath, indexPath)
|
|
383
429
|
|
|
430
|
+
// Note: catchAll is intentional - MCP boundary converts errors to JSON format
|
|
384
431
|
const result = await Effect.runPromise(
|
|
385
432
|
buildIndex(resolvedPath, { force }).pipe(
|
|
386
433
|
Effect.catchAll((e) => Effect.succeed({ error: e.message })),
|
|
@@ -404,6 +451,84 @@ const handleMdIndex = async (
|
|
|
404
451
|
}
|
|
405
452
|
}
|
|
406
453
|
|
|
454
|
+
const handleMdLinks = async (
|
|
455
|
+
args: Record<string, unknown>,
|
|
456
|
+
rootPath: string,
|
|
457
|
+
): Promise<CallToolResult> => {
|
|
458
|
+
const filePath = args.path as string
|
|
459
|
+
const resolvedPath = path.isAbsolute(filePath)
|
|
460
|
+
? filePath
|
|
461
|
+
: path.join(rootPath, filePath)
|
|
462
|
+
|
|
463
|
+
// Note: catchAll is intentional - MCP boundary converts errors to JSON format
|
|
464
|
+
const result = await Effect.runPromise(
|
|
465
|
+
getOutgoingLinks(rootPath, resolvedPath).pipe(
|
|
466
|
+
Effect.catchAll((e) => Effect.succeed({ error: e.message })),
|
|
467
|
+
),
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
if ('error' in result) {
|
|
471
|
+
return {
|
|
472
|
+
content: [{ type: 'text', text: `Error: ${result.error}` }],
|
|
473
|
+
isError: true,
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const links = result as readonly string[]
|
|
478
|
+
const relativePath = path.relative(rootPath, resolvedPath)
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
content: [
|
|
482
|
+
{
|
|
483
|
+
type: 'text',
|
|
484
|
+
text:
|
|
485
|
+
links.length > 0
|
|
486
|
+
? `Outgoing links from ${relativePath}:\n\n${links.map((l) => ` -> ${l}`).join('\n')}\n\nTotal: ${links.length} links`
|
|
487
|
+
: `No outgoing links from ${relativePath}`,
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const handleMdBacklinks = async (
|
|
494
|
+
args: Record<string, unknown>,
|
|
495
|
+
rootPath: string,
|
|
496
|
+
): Promise<CallToolResult> => {
|
|
497
|
+
const filePath = args.path as string
|
|
498
|
+
const resolvedPath = path.isAbsolute(filePath)
|
|
499
|
+
? filePath
|
|
500
|
+
: path.join(rootPath, filePath)
|
|
501
|
+
|
|
502
|
+
// Note: catchAll is intentional - MCP boundary converts errors to JSON format
|
|
503
|
+
const result = await Effect.runPromise(
|
|
504
|
+
getIncomingLinks(rootPath, resolvedPath).pipe(
|
|
505
|
+
Effect.catchAll((e) => Effect.succeed({ error: e.message })),
|
|
506
|
+
),
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
if ('error' in result) {
|
|
510
|
+
return {
|
|
511
|
+
content: [{ type: 'text', text: `Error: ${result.error}` }],
|
|
512
|
+
isError: true,
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const links = result as readonly string[]
|
|
517
|
+
const relativePath = path.relative(rootPath, resolvedPath)
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
content: [
|
|
521
|
+
{
|
|
522
|
+
type: 'text',
|
|
523
|
+
text:
|
|
524
|
+
links.length > 0
|
|
525
|
+
? `Incoming links to ${relativePath}:\n\n${links.map((l) => ` <- ${l}`).join('\n')}\n\nTotal: ${links.length} backlinks`
|
|
526
|
+
: `No incoming links to ${relativePath}`,
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
407
532
|
// ============================================================================
|
|
408
533
|
// MCP Server Setup
|
|
409
534
|
// ============================================================================
|
|
@@ -412,7 +537,7 @@ const createServer = (rootPath: string) => {
|
|
|
412
537
|
const server = new Server(
|
|
413
538
|
{
|
|
414
539
|
name: 'mdcontext-mcp',
|
|
415
|
-
version:
|
|
540
|
+
version: MCP_VERSION,
|
|
416
541
|
},
|
|
417
542
|
{
|
|
418
543
|
capabilities: {
|
|
@@ -441,6 +566,10 @@ const createServer = (rootPath: string) => {
|
|
|
441
566
|
return handleMdKeywordSearch(args ?? {}, rootPath)
|
|
442
567
|
case 'md_index':
|
|
443
568
|
return handleMdIndex(args ?? {}, rootPath)
|
|
569
|
+
case 'md_links':
|
|
570
|
+
return handleMdLinks(args ?? {}, rootPath)
|
|
571
|
+
case 'md_backlinks':
|
|
572
|
+
return handleMdBacklinks(args ?? {}, rootPath)
|
|
444
573
|
default:
|
|
445
574
|
return {
|
|
446
575
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
package/src/parser/parser.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
MdSection,
|
|
22
22
|
ParseError,
|
|
23
23
|
} from '../core/types.js'
|
|
24
|
+
import { FileReadError } from '../errors/index.js'
|
|
24
25
|
import { countTokensApprox, countWords } from '../utils/tokens.js'
|
|
25
26
|
|
|
26
27
|
// ============================================================================
|
|
@@ -362,31 +363,29 @@ export const parse = (
|
|
|
362
363
|
|
|
363
364
|
/**
|
|
364
365
|
* Parse a markdown file from the filesystem
|
|
366
|
+
*
|
|
367
|
+
* @throws ParseError - File content cannot be parsed
|
|
368
|
+
* @throws FileReadError - File cannot be read from filesystem
|
|
365
369
|
*/
|
|
366
370
|
export const parseFile = (
|
|
367
371
|
filePath: string,
|
|
368
|
-
): Effect.Effect<
|
|
369
|
-
MdDocument,
|
|
370
|
-
ParseError | { _tag: 'IoError'; message: string; path: string }
|
|
371
|
-
> =>
|
|
372
|
+
): Effect.Effect<MdDocument, ParseError | FileReadError> =>
|
|
372
373
|
Effect.gen(function* () {
|
|
373
374
|
const fs = yield* Effect.promise(() => import('node:fs/promises'))
|
|
374
375
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
})
|
|
389
|
-
}
|
|
376
|
+
const [content, stats] = yield* Effect.tryPromise({
|
|
377
|
+
try: () =>
|
|
378
|
+
Promise.all([
|
|
379
|
+
fs.readFile(filePath, 'utf-8'),
|
|
380
|
+
fs.stat(filePath),
|
|
381
|
+
] as const),
|
|
382
|
+
catch: (error) =>
|
|
383
|
+
new FileReadError({
|
|
384
|
+
path: filePath,
|
|
385
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
386
|
+
cause: error,
|
|
387
|
+
}),
|
|
388
|
+
})
|
|
390
389
|
|
|
391
390
|
return yield* parse(content, {
|
|
392
391
|
path: filePath,
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for section filtering utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from 'vitest'
|
|
6
|
+
import type { HeadingLevel, MdDocument, MdSection } from '../core/types.js'
|
|
7
|
+
import {
|
|
8
|
+
buildSectionList,
|
|
9
|
+
extractSectionContent,
|
|
10
|
+
filterDocumentSections,
|
|
11
|
+
filterExcludedSections,
|
|
12
|
+
} from './section-filter.js'
|
|
13
|
+
|
|
14
|
+
// Helper to create minimal section for testing
|
|
15
|
+
const createSection = (
|
|
16
|
+
heading: string,
|
|
17
|
+
level: HeadingLevel,
|
|
18
|
+
children: MdSection[] = [],
|
|
19
|
+
tokenCount: number = 100,
|
|
20
|
+
): MdSection => ({
|
|
21
|
+
id: `section-${heading.toLowerCase().replace(/\s+/g, '-')}`,
|
|
22
|
+
heading,
|
|
23
|
+
level,
|
|
24
|
+
content: `# ${heading}\n\nContent for ${heading}`,
|
|
25
|
+
plainText: `Content for ${heading}`,
|
|
26
|
+
startLine: 1,
|
|
27
|
+
endLine: 10,
|
|
28
|
+
children,
|
|
29
|
+
metadata: {
|
|
30
|
+
wordCount: 10,
|
|
31
|
+
tokenCount,
|
|
32
|
+
hasCode: false,
|
|
33
|
+
hasList: false,
|
|
34
|
+
hasTable: false,
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// Helper to create minimal document for testing
|
|
39
|
+
const createDocument = (sections: MdSection[]): MdDocument => ({
|
|
40
|
+
id: 'test-doc',
|
|
41
|
+
path: '/test/doc.md',
|
|
42
|
+
title: 'Test Document',
|
|
43
|
+
sections,
|
|
44
|
+
links: [],
|
|
45
|
+
codeBlocks: [],
|
|
46
|
+
metadata: {
|
|
47
|
+
tokenCount: sections.reduce((acc, s) => acc + s.metadata.tokenCount, 0),
|
|
48
|
+
headingCount: sections.length,
|
|
49
|
+
linkCount: 0,
|
|
50
|
+
codeBlockCount: 0,
|
|
51
|
+
wordCount: 100,
|
|
52
|
+
lastModified: new Date(),
|
|
53
|
+
indexedAt: new Date(),
|
|
54
|
+
},
|
|
55
|
+
frontmatter: {},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('section-filter', () => {
|
|
59
|
+
describe('filterExcludedSections', () => {
|
|
60
|
+
const sectionList = [
|
|
61
|
+
{ number: '1', heading: 'Introduction', level: 1, tokenCount: 100 },
|
|
62
|
+
{ number: '1.1', heading: 'Overview', level: 2, tokenCount: 50 },
|
|
63
|
+
{ number: '2', heading: 'Installation', level: 1, tokenCount: 200 },
|
|
64
|
+
{ number: '2.1', heading: 'Requirements', level: 2, tokenCount: 75 },
|
|
65
|
+
{ number: '2.2', heading: 'Setup Steps', level: 2, tokenCount: 80 },
|
|
66
|
+
{ number: '3', heading: 'API Reference', level: 1, tokenCount: 500 },
|
|
67
|
+
{ number: '3.1', heading: 'Methods', level: 2, tokenCount: 300 },
|
|
68
|
+
{ number: '4', heading: 'License', level: 1, tokenCount: 50 },
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
it('returns all sections when no exclusion patterns provided', () => {
|
|
72
|
+
const result = filterExcludedSections(sectionList, [])
|
|
73
|
+
expect(result).toEqual(sectionList)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('excludes sections by exact heading match', () => {
|
|
77
|
+
const result = filterExcludedSections(sectionList, ['License'])
|
|
78
|
+
expect(result).toHaveLength(7)
|
|
79
|
+
expect(result.find((s) => s.heading === 'License')).toBeUndefined()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('excludes sections by partial heading match', () => {
|
|
83
|
+
const result = filterExcludedSections(sectionList, ['Setup'])
|
|
84
|
+
expect(result).toHaveLength(7)
|
|
85
|
+
expect(result.find((s) => s.heading === 'Setup Steps')).toBeUndefined()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('excludes sections by glob pattern', () => {
|
|
89
|
+
const result = filterExcludedSections(sectionList, ['*Reference*'])
|
|
90
|
+
expect(result).toHaveLength(7)
|
|
91
|
+
expect(result.find((s) => s.heading === 'API Reference')).toBeUndefined()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('excludes sections by section number', () => {
|
|
95
|
+
const result = filterExcludedSections(sectionList, ['2.1'])
|
|
96
|
+
expect(result).toHaveLength(7)
|
|
97
|
+
expect(result.find((s) => s.number === '2.1')).toBeUndefined()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('excludes multiple sections with multiple patterns', () => {
|
|
101
|
+
const result = filterExcludedSections(sectionList, [
|
|
102
|
+
'License',
|
|
103
|
+
'Overview',
|
|
104
|
+
])
|
|
105
|
+
expect(result).toHaveLength(6)
|
|
106
|
+
expect(result.find((s) => s.heading === 'License')).toBeUndefined()
|
|
107
|
+
expect(result.find((s) => s.heading === 'Overview')).toBeUndefined()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('handles case-insensitive matching', () => {
|
|
111
|
+
const result = filterExcludedSections(sectionList, ['LICENSE'])
|
|
112
|
+
expect(result).toHaveLength(7)
|
|
113
|
+
expect(result.find((s) => s.heading === 'License')).toBeUndefined()
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('extractSectionContent with exclusion', () => {
|
|
118
|
+
const doc = createDocument([
|
|
119
|
+
createSection('Introduction', 1, [
|
|
120
|
+
createSection('Getting Started', 2),
|
|
121
|
+
createSection('Quick Start', 2),
|
|
122
|
+
]),
|
|
123
|
+
createSection('API', 1, [
|
|
124
|
+
createSection('Methods', 2),
|
|
125
|
+
createSection('Properties', 2),
|
|
126
|
+
]),
|
|
127
|
+
createSection('License', 1),
|
|
128
|
+
])
|
|
129
|
+
|
|
130
|
+
it('extracts all matching sections without exclusion', () => {
|
|
131
|
+
const result = extractSectionContent(doc, '*')
|
|
132
|
+
expect(result.matchedNumbers).toHaveLength(7)
|
|
133
|
+
expect(result.excludedNumbers).toHaveLength(0)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('excludes sections matching exclusion pattern', () => {
|
|
137
|
+
const result = extractSectionContent(doc, '*', {
|
|
138
|
+
exclude: ['License'],
|
|
139
|
+
})
|
|
140
|
+
expect(result.matchedNumbers).toHaveLength(6)
|
|
141
|
+
expect(result.excludedNumbers).toEqual(['3'])
|
|
142
|
+
expect(
|
|
143
|
+
result.sections.find((s) => s.heading === 'License'),
|
|
144
|
+
).toBeUndefined()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('reports excluded sections in excludedNumbers', () => {
|
|
148
|
+
const result = extractSectionContent(doc, '*', {
|
|
149
|
+
exclude: ['Quick Start', 'Properties'],
|
|
150
|
+
})
|
|
151
|
+
expect(result.excludedNumbers).toContain('1.2')
|
|
152
|
+
expect(result.excludedNumbers).toContain('2.2')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('combines shallow and exclude options', () => {
|
|
156
|
+
const result = extractSectionContent(doc, 'Introduction', {
|
|
157
|
+
shallow: true,
|
|
158
|
+
exclude: ['Getting Started'],
|
|
159
|
+
})
|
|
160
|
+
// With shallow, we only get Introduction without children
|
|
161
|
+
// The exclude pattern only affects the matched sections list
|
|
162
|
+
expect(result.sections).toHaveLength(1)
|
|
163
|
+
expect(result.sections[0]?.heading).toBe('Introduction')
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('filterDocumentSections', () => {
|
|
168
|
+
const doc = createDocument([
|
|
169
|
+
createSection('Introduction', 1, [
|
|
170
|
+
createSection('Overview', 2),
|
|
171
|
+
createSection('Goals', 2),
|
|
172
|
+
]),
|
|
173
|
+
createSection('Installation', 1),
|
|
174
|
+
createSection('License', 1),
|
|
175
|
+
])
|
|
176
|
+
|
|
177
|
+
it('returns original document when no exclusion patterns', () => {
|
|
178
|
+
const result = filterDocumentSections(doc, [])
|
|
179
|
+
expect(result.document).toBe(doc)
|
|
180
|
+
expect(result.excludedCount).toBe(0)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('filters out matching sections from document', () => {
|
|
184
|
+
const result = filterDocumentSections(doc, ['License'])
|
|
185
|
+
expect(result.excludedCount).toBe(1)
|
|
186
|
+
expect(result.document.sections).toHaveLength(2)
|
|
187
|
+
expect(
|
|
188
|
+
result.document.sections.find((s) => s.heading === 'License'),
|
|
189
|
+
).toBeUndefined()
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('filters out nested sections', () => {
|
|
193
|
+
const result = filterDocumentSections(doc, ['Overview'])
|
|
194
|
+
expect(result.excludedCount).toBe(1)
|
|
195
|
+
// Find Introduction section
|
|
196
|
+
const intro = result.document.sections.find(
|
|
197
|
+
(s) => s.heading === 'Introduction',
|
|
198
|
+
)
|
|
199
|
+
expect(intro).toBeDefined()
|
|
200
|
+
// Overview should be removed from children
|
|
201
|
+
expect(
|
|
202
|
+
intro?.children.find((c) => c.heading === 'Overview'),
|
|
203
|
+
).toBeUndefined()
|
|
204
|
+
// Goals should still be there
|
|
205
|
+
expect(intro?.children.find((c) => c.heading === 'Goals')).toBeDefined()
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('filters multiple sections with glob pattern', () => {
|
|
209
|
+
const result = filterDocumentSections(doc, ['*stallation*', 'License'])
|
|
210
|
+
expect(result.excludedCount).toBe(2)
|
|
211
|
+
expect(result.document.sections).toHaveLength(1)
|
|
212
|
+
expect(result.document.sections[0]?.heading).toBe('Introduction')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('preserves document structure for non-matching sections', () => {
|
|
216
|
+
const result = filterDocumentSections(doc, ['NonExistent'])
|
|
217
|
+
expect(result.document).toBe(doc)
|
|
218
|
+
expect(result.excludedCount).toBe(0)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('counts descendants when parent section is excluded', () => {
|
|
222
|
+
// Introduction has 2 children (Overview, Goals), so excluding Introduction
|
|
223
|
+
// should count 3 total excluded sections
|
|
224
|
+
const result = filterDocumentSections(doc, ['Introduction'])
|
|
225
|
+
expect(result.excludedCount).toBe(3) // Introduction + Overview + Goals
|
|
226
|
+
expect(result.document.sections).toHaveLength(2) // Installation + License
|
|
227
|
+
expect(
|
|
228
|
+
result.document.sections.find((s) => s.heading === 'Introduction'),
|
|
229
|
+
).toBeUndefined()
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('counts deeply nested descendants correctly', () => {
|
|
233
|
+
const deepDoc = createDocument([
|
|
234
|
+
createSection('Root', 1, [
|
|
235
|
+
createSection('Child 1', 2, [
|
|
236
|
+
createSection('Grandchild 1', 3),
|
|
237
|
+
createSection('Grandchild 2', 3),
|
|
238
|
+
]),
|
|
239
|
+
createSection('Child 2', 2),
|
|
240
|
+
]),
|
|
241
|
+
createSection('Other', 1),
|
|
242
|
+
])
|
|
243
|
+
const result = filterDocumentSections(deepDoc, ['Root'])
|
|
244
|
+
// Root + Child 1 + Grandchild 1 + Grandchild 2 + Child 2 = 5
|
|
245
|
+
expect(result.excludedCount).toBe(5)
|
|
246
|
+
expect(result.document.sections).toHaveLength(1)
|
|
247
|
+
expect(result.document.sections[0]?.heading).toBe('Other')
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('does not double-count when multiple patterns match same section', () => {
|
|
251
|
+
const result = filterDocumentSections(doc, ['Introduction', 'Intro*'])
|
|
252
|
+
// Both patterns match Introduction, but should only count once
|
|
253
|
+
// Introduction + Overview + Goals = 3
|
|
254
|
+
expect(result.excludedCount).toBe(3)
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
describe('buildSectionList', () => {
|
|
259
|
+
const doc = createDocument([
|
|
260
|
+
createSection('A', 1, [
|
|
261
|
+
createSection('A.1', 2, [createSection('A.1.1', 3)]),
|
|
262
|
+
createSection('A.2', 2),
|
|
263
|
+
]),
|
|
264
|
+
createSection('B', 1),
|
|
265
|
+
])
|
|
266
|
+
|
|
267
|
+
it('assigns correct hierarchical numbers', () => {
|
|
268
|
+
const list = buildSectionList(doc)
|
|
269
|
+
expect(list).toHaveLength(5)
|
|
270
|
+
expect(list[0]).toMatchObject({ number: '1', heading: 'A' })
|
|
271
|
+
expect(list[1]).toMatchObject({ number: '1.1', heading: 'A.1' })
|
|
272
|
+
expect(list[2]).toMatchObject({ number: '1.1.1', heading: 'A.1.1' })
|
|
273
|
+
expect(list[3]).toMatchObject({ number: '1.2', heading: 'A.2' })
|
|
274
|
+
expect(list[4]).toMatchObject({ number: '2', heading: 'B' })
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
})
|