Skip to content

MCP Tools

Lien exposes six tools through the Model Context Protocol (MCP), available in Cursor, Claude Code, and other MCP-compatible AI assistants.

search_code

Full-text keyword search over your codebase (FTS5/BM25). Despite the name, this is lexical search: it does not embed your query. It matches query terms against symbol names, identifier-split symbol tokens, and chunk content (including comments/docstrings), ranked by BM25.

Keyword search, not meaning search

Query with concrete keywords and identifiers that appear in the code, not natural-language questions. A paraphrase that shares no words with the code will not match (e.g. "auth" will not surface login/hashPassword). See How It Works for why Lien is lexical rather than semantic. For an exact symbol name, prefer list_functions.

Parameters

ParameterTypeRequiredDefaultDescription
querystringYes-Keyword search query (identifiers and domain terms that appear in the code)
limitnumberNo5Maximum number of results to return

Usage

Search for "authenticate user session token"
Find code with terms: jwt token validation verify

Response

json
{
  "results": [
    {
      "content": "async function authenticateUser(credentials) { ... }",
      "score": 0.94,
      "relevance": "highly_relevant",
      "metadata": {
        "file": "src/auth/authenticate.ts",
        "startLine": 23,
        "endLine": 45,
        "symbolName": "authenticateUser",
        "dependentCount": 12
      }
    }
  ]
}

Ranking, and what dependentCount means

Results are ordered by BM25, then nudged by structural importance: a file that more other files import ranks slightly higher among similarly-relevant matches. Results are also nudged down slightly when the file is a test file (a fixed 20% demotion) — test helpers and fixtures otherwise tend to out-rank the real source they exist to test, since they're often lexically strong (they repeat the vocabulary under test) and well-connected (heavily cross-referenced by other tests). This is a nudge, never a filter: a query naming a test file directly still finds it. Both nudges are capped/small and usually only break ties, but a very well-connected hub file can outrank a marginally better lexical match. Each result's own score/relevance always describe its pure lexical match quality and are never recomputed from either nudge, so list order and an individual result's relevance label can legitimately disagree — trust the order for "what to look at first", relevance for "how good is this specific match". Set LIEN_STRUCTURAL_RANKING=off or LIEN_TEST_FILE_RANKING=off to disable either nudge independently and fall back toward pure BM25.

dependentCount is how many other indexed files import this file, resolved with the same import-matching rules get_dependents uses. It is a floor, not get_dependents' authoritative count: it is file-level only and does not follow re-export/barrel chains, so a module fronted by a barrel can read lower than its real blast radius.

A present 0 means "resolved, and no other indexed file imports this file" — a real answer, though still a floor. The field is absent whenever that number would not mean anything, because an omitted count is honest and a wrong one isn't. Absence means "unknown", never 0, and happens in two situations:

  • This file's language cannot name it in an import at all. C#'s global using / enclosing-namespace access, Java's and Kotlin's same-package visibility, and Swift's whole-module import Foundation style all let a real caller reach a file with no import statement naming it. These are the same languages get_dependents flags with dependent-attribution-incomplete, and the omission fires under the same condition: a zero count in one of them. A positive count in those languages is kept — it is a genuine recovered floor.
  • The whole index predates count tracking, so nothing was ever computed and every count would read 0. This one also comes with an explicit note telling you to run lien index; it clears permanently after one index run.

The counts are precomputed at index time, not per query, and are refreshed by a full index run (and, in a linked worktree, by an overlay rebuild) rather than by every incremental single-file update — so a count can lag your working tree by at most one full index run. That is an accepted trade for a soft ranking tie-breaker, and it is deliberately not warned about per call. When you need an authoritative, current answer, call get_dependents, which resolves on demand and carries its own attributionCaveat vocabulary.

Best Practices

  • Query with keywords and identifiers that appear in the code, not questions
  • Include several related terms: they are OR-joined, and BM25 ranks multi-term matches highest
  • Lean on domain vocabulary the code actually uses ("token", "session", "retry", "backoff")
  • For an exact symbol name, use list_functions; for exact literal strings, use grep
  • Increase limit for broader exploration (up to 15)

Examples

Good queries:

  • "authenticate user session token"
  • "validate email address regex"
  • "payment transaction charge refund"
  • "parse json response body"
  • "authorization middleware guard"
  • "harness evidence gate skip label" (also matches YAML config, e.g. a GitHub Actions workflow step)

Poor queries:

  • "how does login work?" (a question: use the code's own terms instead)
  • "is the user allowed in?" (paraphrase: no shared vocabulary with the code)
  • "code" (way too generic)

find_similar

Find code similar to a given snippet.

Parameters

ParameterTypeRequiredDefaultDescription
codestringYes-Code snippet to find similar implementations (min 24 chars)
limitnumberNo5Maximum number of results to return
languagestringNo-Filter by programming language (e.g., "typescript", "python")
pathHintstringNo-Filter by file path substring (e.g., "src/api", "components")

Usage

Find similar code to this function:
async function fetchUser(id: string) {
  return await db.users.findById(id);
}
Find similar TypeScript code in the API directory:
find_similar({
  code: "async function fetchUser(id: string) { ... }",
  language: "typescript",
  pathHint: "src/api"
})

Response

Similar format to search_code. Matching is lexical (BM25 over the snippet's tokens), not semantic: it finds code that shares identifiers and keywords with your snippet.

When filters are applied or low-relevance results are pruned, the response includes:

json
{
  "filtersApplied": {
    "language": "typescript",
    "pathHint": "src/api",
    "prunedLowRelevance": 3
  }
}

Automatic Pruning

Low-relevance results (not_relevant category) are automatically removed to reduce noise. The prunedLowRelevance count shows how many were removed.

get_files_context

Get all chunks and related context for one or more files.

Parameters

ParameterTypeRequiredDefaultDescription
filepathsstring | string[]Yes-Path(s) to file (relative to project root). Single path or array of paths (max 50).
includeRelatedbooleanNotrueInclude related chunks from other files

Usage

Show context for src/utils/auth.ts
Get context for multiple files: ["src/auth.ts", "src/user.ts"]
Get file context for app/Models/User.php without related files

Response

json
{
  "file": "src/utils/auth.ts",
  "chunks": [
    {
      "content": "export function validateToken(token: string) { ... }",
      "startLine": 1,
      "endLine": 15,
      "score": 0.0
    }
  ],
  "testAssociations": ["src/utils/auth.test.ts"],
  "relatedChunks": [
    {
      "content": "import { validateToken } from './auth';",
      "file": "src/middleware/auth.ts",
      "startLine": 1,
      "endLine": 1,
      "score": 0.45,
      "relevance": "highly_relevant"
    }
  ]
}

Features

  • Returns all chunks from the specified file(s)
  • Includes test associations (which tests cover this file)
  • Optionally includes related chunks from other files
  • Useful before editing a file to understand dependencies
  • Supports batch operations for multiple files (up to 50)
  • When a function in the file is at or above 80% of its cyclomatic or cognitive complexity threshold, the response also includes a complexityHeadroom array (each entry has symbol, metric, value, threshold; capped at 5 per file, with complexityHeadroomMore giving the overflow count) plus a complexityHeadroomWarning string, a one-line imperative summary of the same data that appears before complexityHeadroom in the response. Both fields are omitted when nothing is near budget.

Response Format

For a single file, returns:

json
{
  "indexInfo": { ... },
  "file": "src/utils/auth.ts",
  "chunks": [ ... ]
}

For multiple files, returns:

json
{
  "indexInfo": { ... },
  "files": {
    "src/auth.ts": { "chunks": [ ... ] },
    "src/user.ts": { "chunks": [ ... ] }
  }
}

list_functions

List functions, classes, and interfaces by name pattern.

Parameters

ParameterTypeRequiredDefaultDescription
patternstringNo-Regex pattern to match symbol names
languagestringNo-Filter by language (e.g., "typescript", "python")
symbolTypeenumNo-Filter by symbol type: function, method, class, or interface
limitnumberNo50Number of results to return (max 200)
offsetnumberNo0Skip first N results for pagination

Usage

List all functions matching ".*Controller$"
Show all TypeScript classes

Response

json
{
  "indexInfo": { "indexVersion": 1234567890, "indexDate": "2025-12-19" },
  "results": [
    {
      "content": "...",
      "score": 0,
      "relevance": "not_relevant",
      "metadata": {
        "symbolName": "UserController",
        "symbolType": "class",
        "file": "src/controllers/UserController.ts",
        "startLine": 10,
        "endLine": 85,
        "language": "typescript"
      }
    }
  ],
  "method": "symbols",
  "hasMore": true,
  "nextOffset": 50
}

Use Cases

  • Architecture Overview: List all Controllers, Services, Models
  • Pattern Discovery: Find functions matching naming conventions
  • Quick Navigation: Locate specific classes or functions by name

Examples

  • Find all Controllers: pattern: ".*Controller.*"
  • Find all Services: pattern: ".*Service$"
  • Find all API handlers: pattern: "handle.*"
  • Find all TypeScript utilities: pattern: ".*", language: "typescript"

get_dependents

Find all files that depend on a given file (reverse dependency lookup). Essential for impact analysis before refactoring.

Parameters

ParameterTypeRequiredDefaultDescription
filepathstringYes-Path to file (relative to project root)
depthnumberNo1Dependency depth (currently only 1 supported)
symbolstringNo-Specific exported symbol to find usages of (returns call sites instead of just importing files)

Usage

What depends on src/utils/validate.ts?
Is it safe to change this file?

Response

json
{
  "indexInfo": { "indexVersion": 1234567890, "indexDate": "2025-12-19" },
  "filepath": "src/utils/validate.ts",
  "dependentCount": 12,
  "productionDependentCount": 9,
  "testDependentCount": 3,
  "riskLevel": "medium",
  "dependents": [
    { "filepath": "src/api/users.ts", "isTestFile": false },
    { "filepath": "src/api/auth.ts", "isTestFile": false },
    { "filepath": "src/__tests__/validate.test.ts", "isTestFile": true }
  ],
  "complexityMetrics": {
    "averageComplexity": 6.2,
    "maxComplexity": 15,
    "filesWithComplexityData": 10,
    "highComplexityDependents": [
      { "filepath": "src/api/users.ts", "maxComplexity": 15, "avgComplexity": 8.3 }
    ],
    "complexityRiskBoost": "medium"
  }
}

When symbol is provided, the response also includes totalUsageCount (number of tracked call sites across all files) and each dependent may include a usages array with callerSymbol, line, and snippet fields.

For a type-shaped symbol (class/struct/interface/enum — see type-symbol-attribution-incomplete below), the response may also include importedBy: a list of files (always a subset of dependents) that the call-site-level dependency graph can verify import the symbol even though no literal call site names it. importedBy carries no line number — only dependents[].usages does, and only for a real call site.

attributionCaveat

Five unrelated situations can each make dependentCount/riskLevel untrustworthy as a verified clear. Rather than five differently-named flags, the response carries a single optional field:

json
{
  "attributionCaveat": {
    "reason": "unresolved-target",
    "note": "..."
  }
}

reason is one of:

  • unresolved-targetfilepath isn't resolvable in the index at all: never indexed, misspelled, or a typo'd directory prefix. dependentCount: 0 / riskLevel: "low" then means "the path is unresolved," not "confirmed zero dependents." (Two independent checks can produce this — the index manifest has no entry for the path at all, or the path resolves in the manifest but has zero chunks in the current scan — but only one attributionCaveat is ever returned, never two competing explanations of the same zero.)
  • symbol-attribution-degradedsymbol also accepts a method or constructor name (e.g. __construct, moveUp); those aren't top-level exports, so when call sites for one can't be confirmed, the response widens dependentCount/riskLevel to the file-level answer (every file that imports filepath) rather than asserting an unverifiable symbol-scoped count. The unconfirmed symbol may genuinely be a method/constructor, or it may be a typo'd/hallucinated/removed name — the note field says which.
  • type-symbol-attribution-incompletesymbol IS a top-level export of filepath, but it names a class/struct/interface/enum declaration rather than a function or method (#1015). Usage attribution is call-site-driven, and nothing "calls" a type by its own name the way a function call does — constructor calls, type hints, extends/implements clauses, generic type arguments, and dependency-injected property access don't reliably surface as a tracked call site. totalUsageCount/usages are a partial, best-effort floor — often 0 even when real usages exist — never a verified total. When the call-site-level dependency graph can independently verify a non-call-site import for the symbol anyway (a constructor call, a type hint), the response's importedBy field names those files — deliberately with no line number attached, since that would have to be fabricated. dependentCount/dependents (which files import the symbol) remain reliable — unless filepath's language also has an import-invisible same-unit access shape (C#, Java, Kotlin, Swift — #1005's same-package/whole-module blind spot), in which case the note says so explicitly instead of asserting a reliability it doesn't have (#1057).
  • dependent-attribution-partial — a file-level query (no symbol) found zero import-based dependents, but a lower-confidence non-import fallback recovered one or more anyway. Those entries carry confidence: "inferred" in dependents[], plus inferredVia naming which fallback found them (C# has one, for the global using gap; Go has one, for a bare module-root self-import; Java/Kotlin have one, for same-package visibility — #1005). Treat dependentCount/riskLevel as a recovered floor, not a verified/complete answer — the note spells out what that specific fallback can still miss, and each fallback misses something different.
  • dependent-attribution-incomplete — a query — file-level (no symbol) or symbol-level — found zero dependents in a language where the import graph structurally can't see every real usage — C#'s enclosing-namespace access, where a global using lets a real caller reach filepath's exports with no per-file import at all (#930); or Java/Kotlin's same-package visibility and Swift's whole-module access (#1005) — even after the dependent-attribution-partial fallback above also found nothing. dependentCount: 0 / riskLevel: "low" here means "the import graph found nothing," not "nothing depends on this file." Also fires for a symbol-scoped query on a real, non-type-declaration export (#1097) — e.g. a Java method with genuine same-package callers the import graph can't see — unless type-symbol-attribution-incomplete already explains the same zero.

At most one reason ever applies to a given response. Always check for attributionCaveat before treating a low (especially zero) dependentCount as a verified all-clear.

Risk Levels

LevelDependent CountMeaning
low0-5Safe to change, few dependents
medium6-15Review dependents before changing
high16-30Careful planning needed
critical30+Major impact, extensive testing required

Complexity-Aware Risk

Risk level is boosted if dependents have high complexity. A file with 10 dependents but complex dependent code may be rated "high" instead of "medium". A high/critical complexity signal among dependents always lifts the level above "low", even when every dependent is fully tested — test coverage lowers the odds of a silent break, it doesn't shrink the blast radius.

get_complexity

Analyze code complexity for tech debt identification and refactoring prioritization. Tracks four metrics (cyclomatic, cognitive, Halstead effort, Halstead bugs). See Configuration for what each one measures and how to set thresholds.

Parameters

ParameterTypeRequiredDefaultDescription
filesstring[]No-Specific files to analyze (analyzes all if omitted)
topnumberNo10Return top N most complex functions
thresholdnumberNoconfigOnly return functions above this complexity

Usage

What are the most complex functions in this codebase?
Show me tech debt hotspots
Analyze complexity of src/api/

Response

json
{
  "summary": {
    "filesAnalyzed": 156,
    "avgComplexity": 4.2,
    "maxComplexity": 23,
    "violationCount": 8,
    "bySeverity": { "error": 3, "warning": 5 }
  },
  "violations": [
    {
      "filepath": "src/parser/index.ts",
      "symbolName": "parseComplexExpression",
      "symbolType": "function",
      "startLine": 45,
      "endLine": 120,
      "complexity": 23,
      "threshold": 15,
      "severity": "error",
      "metricType": "cyclomatic",
      "language": "typescript",
      "message": "Cyclomatic complexity 23 exceeds threshold 15",
      "dependentCount": 5,
      "complexityRiskLevel": "high"
    },
    {
      "filepath": "src/parser/index.ts",
      "symbolName": "parseComplexExpression",
      "symbolType": "function",
      "startLine": 45,
      "endLine": 120,
      "complexity": 97200,
      "threshold": 64800,
      "severity": "warning",
      "metricType": "halstead_effort",
      "language": "typescript",
      "message": "Time to understand ~1h 30m exceeds threshold 1h",
      "dependentCount": 5,
      "complexityRiskLevel": "medium",
      "halsteadDetails": {
        "volume": 850.5,
        "difficulty": 45.2,
        "effort": 97200,
        "bugs": 0.283
      }
    }
  ]
}

complexityRiskLevel is not get_dependents' riskLevel

complexityRiskLevel is this file's OWN complexity severity, boosted (never downgraded) by its dependent count/complexity — there's no test-coverage term in the formula at all. get_dependents's (and lien annotate's, and lien api-delta's) riskLevel is a different metric — blast-radius risk, which weighs dependents' test coverage and applies a complexity floor instead. The two can disagree for the same file at the same moment by design; don't assume they should match.

Metric Types

metricTypeDescription
cyclomaticTest cases needed for full branch coverage
cognitiveMental load - how hard to follow (penalizes nesting)
halstead_effortTime to understand (shown as human-readable duration)
halstead_bugsEstimated bug count (Effort^(2/3) / 3000)

Halstead Metrics

Both Halstead metrics have configurable thresholds (timeToUnderstandMinutes, estimatedBugs). See Configuration for defaults and how to set them.

Severity Levels

SeverityComplexityAction
warning15-29Consider refactoring
error30+Should refactor

Examples

Get top 20 most complex functions
Analyze complexity of src/api/ directory
Show functions with complexity > 15

Understanding Relevance Categories

All search tools include a relevance category alongside a numeric score. Both are derived from the BM25 rank: each result's rank is compared to the best hit in the result set, producing a category and a lower-is-better score (best hit ≈ 0). An exact match on a symbol name is always promoted to highly_relevant.

CategoryMeaning
highly_relevantStrong BM25 match relative to the best hit (or an exact symbol-name match)
relevantGood match, useful context for the query
loosely_relatedWeaker match, may provide background context
not_relevantWeak match, automatically filtered out of results

TIP

Because bands are relative to the best hit in each result set, the top result is always highly_relevant. Categories are keyword-match strength, not semantic similarity: a match means your query terms appear in the code or its comments.

Test Associations

get_files_context and get_complexity return test associations as a flat array of test-file paths:

json
{
  "file": "src/auth/login.ts",
  "testAssociations": ["src/auth/login.test.ts"]
}

search_code does not return this field.

How associations are found

Every entry is derived statically — nothing here comes from executing tests or reading a coverage report:

  • Import-based matching (all languages): the test file's own import statements resolve to the source file. This is the primary mechanism.
  • Same-directory basename (Go): foo.gofoo_test.go, no import needed.
  • Same-package basename (Java): the same pairing across a package's test source set.
  • Enclosing-namespace type references (C#): a test referencing a type that exactly one project file declares, for the global using case where no import statement names the file.

There are no per-association confidence levels

Each entry is a path, and every entry in this field is one the mechanisms above established. There is no confidence or method field on an association, so don't branch on one.

Some lower-confidence mechanisms deliberately stay out of this field rather than being folded in unlabelled — Go package-level fallbacks and Swift symbol-usage matching among them. Those surface only in lien annotate's output, which labels each tier in prose (for example "Test coverage inferred from symbol usage (not import-verified)"). A language whose imports structurally cannot answer the question says so there too, rather than reporting a bare empty list.

Tool Selection Guide

Use search_code when:

  • Discovering code by keyword: identifiers and domain terms that appear in the source
  • You need to find where a concept lives before editing (query with the code's own vocabulary)
  • Looking for patterns, implementations, handlers, validators by their terminology

Use list_functions when:

  • User asks "show me all Controllers" or similar structural queries
  • Looking for classes/functions matching a naming pattern
  • Getting architectural overview

Use get_files_context when:

  • You identified a file via search and need to understand it
  • About to edit a file (check dependencies first)
  • Need to understand test coverage
  • Reviewing multiple files together (e.g., PR review)

Use find_similar when:

  • Refactoring multiple similar pieces of code
  • Ensuring new code matches existing patterns
  • Finding duplicated logic

Use get_dependents when:

  • Checking impact before modifying a file
  • Determining if a file is safe to delete
  • Planning refactoring scope
  • Understanding how changes will propagate

Use get_complexity when:

  • Identifying tech debt hotspots
  • Prioritizing refactoring efforts
  • Reviewing code quality in a PR
  • Tracking codebase health over time

Performance Tips

  1. Start broad: Use search_code with a higher limit (10-15) for exploration
  2. Use the code's words: query with identifiers and domain terms that appear in the source, not paraphrases
  3. Use context: Check related files with get_files_context before editing
  4. Chain tools: search → get context → check dependents → make changes is a powerful pattern

Error Handling

"Index not found"

The MCP server will automatically index your project on first use. If you see this error, try running lien index manually in your project directory.

"No results found"

  • Try broader queries
  • Check if the code is indexed (not in exclude patterns)
  • Rebuild the index: lien index --force

"Invalid file path"

Use paths relative to project root, not absolute paths.

Supported AI Assistants

Lien works with any MCP-compatible AI assistant. See Getting Started for the per-editor setup table (Cursor, Claude Code, Windsurf, OpenCode, Kilo Code, Antigravity, and other MCP clients).

Released under the AGPL-3.0 License. Free forever for local use.