Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

What is ty-find?

ty-find (tyf) is an LSP adapter that lets AI coding agents query Python’s type system by symbol name. It wraps ty’s LSP server so that tyf show MyClass returns the definition location, type signature, and all references — without requiring file paths or line numbers. A background daemon keeps responses under 100ms.

Built for Claude Code, Codex, Cursor, Gemini CLI — and humans who want fast Python navigation from the terminal.

Why tyf?

vs grep/ripgrep

grep matches text. tyf understands Python’s type system.

When you grep for calculate_sum, you get hits in comments, docstrings, string literals, and variable names that happen to contain the substring. tyf returns only the actual symbol definition, its type signature, and where it’s referenced — because it uses ty’s type inference engine under the hood.

vs raw LSP (in editors)

LSP servers are the gold standard for code intelligence, but they require file positions (file.py:29:7) to answer queries. An LLM doesn’t know positions — it thinks in symbol names (MyClass). To use an LSP, it would first need to grep for the position, which is imprecise and adds a round-trip.

tyf breaks this cycle: symbol name in → structured LSP knowledge out. No file paths, no line numbers, no grep step needed.

vs ty check

tyf surfaces ty’s navigation and symbol knowledge — definitions, signatures, and references — not ty’s type-checking diagnostics. So the false-positive noise that affects ty check on dynamic frameworks (Pydantic, SQLAlchemy, and similar) never appears in tyf output, because tyf doesn’t report type errors at all. See Relationship to ty for how the two tools relate.

Installation

ty-find requires ty to be installed and on PATH.

# Install ty (required)
uv add --dev ty

# Install ty-find
uv add --dev ty-find

Quick start

# Definition + signature (default)
$ tyf show list_animals
# Definition (func)
main.py:14:1

# Signature
def list_animals(animals: list[Animal]) -> None

# Refs: 2 across 1 file(s)

# Everything: docs + refs + test refs
$ tyf show list_animals --all

# See all commands
$ tyf --help

For AI agents

Machine-readable documentation is available at llms.txt and llms-full.txt, following the llms.txt convention.

Setup with Claude Code

The main use case for ty-find is giving AI coding agents precise Python symbol navigation. This page explains how to configure Claude Code to prefer tyf over grep for symbol lookups.

CLAUDE.md snippet

Add this to your project’s CLAUDE.md file:

### Python Symbol Navigation — `tyf`

This project has `tyf` — a type-aware code search that gives LSP-quality
results by symbol name. Use `tyf` instead of grep/ripgrep for Python symbol lookups.

- `tyf show my_function` — definition + signature (add `-d` docs, `-r` refs, `-t` test refs, or `--all`)
- `tyf find MyClass` — find definition location
- `tyf refs my_function` — all usages (before refactoring)
- `tyf members TheirClass` — class public API
- `tyf calls my_function` — call tree (`--in` for callers, before refactoring)
- `tyf list file.py` — file outline

All commands accept multiple symbols — batch to save tool calls.
Run `tyf <cmd> --help` for options.

Use grep for: string literals, config values, TODOs, non-Python files.

Permissions

Claude Code will prompt you for permission the first time it tries to run tyf. To avoid repeated prompts, add a Bash permission rule in your project’s .claude/settings.json:

{
  "permissions": {
    "allow": [
      "Bash(tyf:*)"
    ]
  }
}

This allows Claude Code to run any tyf command without asking each time.

Why the strong language?

Claude Code’s system prompt tells it to use its built-in Grep tool for searching code. This is a sensible default — Grep works everywhere and requires no setup.

The problem goes deeper than precision. To use an LSP (the gold standard for code intelligence), an LLM first needs a file position — but it doesn’t know the position without searching. So it greps, gets imprecise results, and has to validate them — a circular round-trip that wastes tokens and time.

tyf breaks this cycle: the LLM passes a symbol name, and tyf resolves the position internally, returning structured LSP results directly. No grep step, no position guessing, no validation loop.

On top of that, grep is a text search tool — it returns false positives from comments, docstrings, and string literals. It can’t tell you a symbol’s type or find all references through the type system.

The CLAUDE.md snippet uses emphatic language (“Use tyf instead of grep”) because that’s what it takes to override a strong system-level preference. Softer phrasing like “consider using tyf” gets ignored in practice.

Priming a new session

In the first Claude Code session with a new project, prime Claude by asking it to run:

uv run tyf guide

It prints a short, agent-facing page: the command reference when tyf is already installed in the project, or the setup steps (including the CLAUDE.md snippet above) when it is not. In a project without tyf, uvx ty-find guide does the same with nothing installed. Neither starts the daemon. tyf --help remains the fallback for a full option listing.

AGENTS.md for other tools

If you use Cursor, Codex, Gemini CLI, or another AI coding tool, the same instructions work — just put them in the file your tool reads:

ToolFile
Claude CodeCLAUDE.md
Cursor.cursorrules
CodexAGENTS.md
Gemini CLIGEMINI.md

If you use multiple tools, you can maintain one file and symlink:

# Write instructions in CLAUDE.md, symlink for others
ln -s CLAUDE.md AGENTS.md
ln -s CLAUDE.md .cursorrules

How It Works

ty-find is built as a three-layer system: a thin frontend, a persistent background daemon, and the ty LSP server that does the actual Python analysis. This page explains how they fit together and why.

There are two frontends — the CLI and the MCP server (tyf mcp) — and they are peers. Both parse a request, hand it to the same daemon over the same socket, and render the result with the same formatter. Neither does any Python analysis of its own, and they share one warm index.

Architecture overview

graph TB
    subgraph Frontends
        CLI["<b>tyf</b> CLI<br/><small>parse args · format output</small>"]
        MCP["<b>tyf mcp</b><br/><small>MCP over stdio · same formatter</small>"]
    end

    subgraph Daemon ["Daemon (background process)"]
        Router["Request Router"]
        subgraph Pool ["LSP Client Pool"]
            CA["TyLspClient A"]
            CB["TyLspClient B"]
        end
    end

    subgraph LSP ["ty LSP servers"]
        LA["ty lsp<br/><small>workspace A</small>"]
        LB["ty lsp<br/><small>workspace B</small>"]
    end

    CLI -- "JSON-RPC 2.0<br/>Unix socket" --> Router
    MCP -- "JSON-RPC 2.0<br/>Unix socket" --> Router
    Router --> CA
    Router --> CB
    CA -- "LSP protocol<br/>stdin/stdout" --> LA
    CB -- "LSP protocol<br/>stdin/stdout" --> LB

Each layer has a single responsibility:

LayerResponsibility
CLI (tyf)Parse arguments, connect to daemon, format output
MCP server (tyf mcp)Serve tool calls over stdio, connect to daemon, format output
DaemonKeep LSP servers alive between calls, route requests
ty LSPPython type analysis, symbol resolution, indexing

Request lifecycle

Here’s what happens when you run tyf find calculate_sum:

sequenceDiagram
    participant CLI as tyf CLI
    participant D as Daemon
    participant LSP as ty LSP server

    CLI->>D: 1. Connect (Unix socket)
    CLI->>D: 2. JSON-RPC "workspace_symbols" request

    Note over D: 3. Look up workspace<br/>in client pool (hit → reuse)

    D->>LSP: 4. workspace/symbol
    LSP-->>D: 5. SymbolInformation[]

    D-->>CLI: 6. JSON-RPC response

    Note over CLI: 7. Format & print results

Note: The diagram above shows the default project-wide search path. When using tyf find --file <path>, the CLI sends a definition request instead, and the daemon uses textDocument/definition to resolve the symbol at a specific file position.

Steps 1–7 take 50–100 ms on a warm daemon. Without the daemon, every call would pay the full LSP startup cost (several seconds).

The daemon

The daemon is a long-running background process that listens on a Unix domain socket at /tmp/ty-find-{uid}.sock. It starts automatically on first use and shuts itself down after 5 minutes of inactivity.

Why a daemon?

Starting an LSP server is expensive. The ty LSP process needs to:

  1. Spawn and initialize
  2. Index the Python project (parse files, resolve imports, build type information)
  3. Reach a “ready” state where it can answer queries

This takes 1–5 seconds depending on project size. The daemon pays this cost once and keeps the server running for subsequent calls.

gantt
    title Without daemon — cold start every time
    dateFormat X
    axisFormat %s

    section tyf find foo
    spawn ty lsp     :a1, 0, 800
    LSP initialize   :a2, 800, 1000
    index project    :a3, 1000, 3000
    LSP query        :a4, 3000, 3050
    format output    :a5, 3050, 3060

    section tyf find bar
    spawn ty lsp     :b1, 3060, 3860
    LSP initialize   :b2, 3860, 4060
    index project    :b3, 4060, 6060
    LSP query        :b4, 6060, 6110
    format output    :b5, 6110, 6120
gantt
    title With daemon — warm after first call
    dateFormat X
    axisFormat %s

    section tyf find foo
    connect to daemon :a1, 0, 20
    send request      :a2, 20, 30
    LSP query         :a3, 30, 430
    format output     :a4, 430, 440

    section tyf find bar
    connect to daemon :b1, 440, 460
    send request      :b2, 460, 470
    LSP query         :b3, 470, 900
    format output     :b4, 900, 910

Auto-start and version checking

The CLI automatically manages the daemon lifecycle:

flowchart TD
    A["tyf find foo"] --> B{"Is daemon<br/>running?"}
    B -- No --> C["Spawn daemon"]
    C --> D["Wait for ready"]
    D --> G["Send request"]
    B -- Yes --> E["Ping daemon"]
    E --> F{"Version<br/>matches?"}
    F -- Yes --> G
    F -- No --> H["Stop old daemon"]
    H --> C

When you upgrade ty-find, the CLI detects that the running daemon is from an older version and restarts it automatically.

Idle shutdown

The daemon tracks activity at two levels:

  • Per-workspace: Each LSP client records its last access time. Clients idle for more than 5 minutes are cleaned up (the ty lsp process is terminated).
  • Daemon-wide: If all workspace clients are idle, the daemon shuts itself down.
timeline
    title Daemon lifecycle example
    0 sec : tyf find foo
          : Daemon starts
          : Workspace A client created
    2 sec : tyf refs bar
          : Workspace A client reused
    30 sec : tyf find baz
           : Workspace A client reused
           : Idle timer reset
    5 min 30 sec : No activity for 5 min
                 : Workspace A client removed
                 : No clients remain
                 : Daemon shuts down

LSP client pool

The daemon maintains a pool of LSP clients, one per workspace. When a request arrives, the daemon resolves it to a workspace root and looks up the corresponding client.

flowchart TD
    R["Incoming request<br/><code>workspace: /home/user/project</code>"] --> L{"Lookup workspace<br/>in HashMap<br/><small>(lock held)</small>"}
    L -- Hit --> RET["Return existing client"]
    L -- Miss --> REL["Release lock"]
    REL --> SPAWN["Spawn ty lsp<br/>Initialize LSP<br/><small>(async, no lock held)</small>"]
    SPAWN --> RELOCK["Re-acquire lock"]
    RELOCK --> CHECK{"Check again<br/><small>(another task may<br/>have created it)</small>"}
    CHECK -- Already exists --> RET
    CHECK -- Still missing --> INS["Insert new client"] --> RET

The pool uses a lock-free fast path pattern: the std::sync::Mutex is held only for the HashMap lookup (microseconds), then dropped before any async work. This avoids holding a lock across .await, which would block other tasks.

Communication protocols

CLI ↔ Daemon: JSON-RPC 2.0 over Unix socket

The CLI and daemon communicate using JSON-RPC 2.0 with LSP-style message framing:

Content-Length: 128\r\n
\r\n
{"jsonrpc":"2.0","id":1,"method":"definition","params":{...}}

Available RPC methods:

MethodDescription
pingHealth check (returns version and uptime)
shutdownGracefully stop the daemon
definitionGo to definition of a symbol at a position
hoverGet type information for a symbol at a position
referencesFind all references to a symbol
batch_referencesFind references for multiple symbols in one call
workspace_symbolsSearch for symbols by name across the workspace
document_symbolsList all symbols in a file
inspectCombined hover + references (definitions resolved client-side via workspace symbols)
membersPublic interface of a class
call_hierarchyWalk the call graph from one or more positions, recursively (the whole tree is built daemon-side)
diagnosticsType errors in a file

Daemon ↔ ty LSP: LSP protocol over stdin/stdout

The daemon communicates with each ty lsp process using the standard Language Server Protocol. Messages use the same Content-Length framing but carry standard LSP methods like textDocument/definition and textDocument/hover.

sequenceDiagram
    participant D as Daemon
    participant LSP as ty lsp (child process)

    D->>LSP: stdin: LSP request (JSON-RPC 2.0)
    LSP-->>D: stdout: LSP response

Response routing works through request IDs: each outgoing request gets a unique integer ID (from an AtomicU64). A background task reads responses from stdout and matches them to pending requests using a HashMap<u64, oneshot::Sender>.

sequenceDiagram
    participant Caller as send_request()
    participant Map as pending_requests
    participant Stdin as stdin (to ty)
    participant Handler as response_handler
    participant Stdout as stdout (from ty)

    Caller->>Map: store tx for id=42
    Caller->>Stdin: write JSON-RPC {id: 42, ...}
    Note over Caller: await rx...

    Stdout-->>Handler: read {id: 42, result: ...}
    Handler->>Map: remove(42) → tx
    Handler->>Caller: tx.send(response)
    Note over Caller: unblocked!

Concurrency model

All LSP parallelism is handled by the daemon, not the frontends:

  • The LSP protocol runs over a single stdin/stdout pipe per server, so requests are inherently sequential.
  • Multi-symbol operations (like tyf show A B C) are sent as a single batch RPC call. The daemon processes them sequentially on its LSP client and returns merged results. Batch your symbols rather than issuing one request per symbol — that is where the win is.
  • The CLI runs one command per process and never opens concurrent connections. The MCP server may: a harness can pipeline tool calls, and each is served on its own task. That is safe because the daemon serializes LSP work anyway — but daemon startup is gated within the process, so concurrent first calls wait for one daemon to come up instead of each spawning their own.
sequenceDiagram
    participant CLI as tyf CLI
    participant D as Daemon
    participant LSP as ty LSP

    CLI->>D: show [A, B, C]
    D->>LSP: hover(A)
    LSP-->>D: result A
    D->>LSP: hover(B)
    LSP-->>D: result B
    D->>LSP: hover(C)
    LSP-->>D: result C
    D-->>CLI: [results for A, B, C]

Document tracking

The LSP protocol requires that a client sends textDocument/didOpen before querying a file, and only sends it once per file per session. The LSP client tracks opened documents in a HashSet<String>:

flowchart TD
    Q["Query for src/main.py:10:5"] --> C{"URI in<br/>opened_documents?"}
    C -- No --> O["Send didOpen"]
    O --> ADD["Add URI to set"]
    ADD --> DEF["Send textDocument/definition"]
    C -- Yes --> DEF

Sending a duplicate didOpen would cause the LSP server to re-analyze the file, returning null results during the re-analysis window. The tracking set prevents this.

Warmup and retries

On a cold start, the LSP server may not be fully ready to answer queries even after initialization completes. The daemon handles this with automatic retries:

sequenceDiagram
    participant D as Daemon
    participant LSP as ty LSP

    Note over D,LSP: First query after daemon start

    D->>LSP: hover(symbol)
    LSP-->>D: null (still indexing)
    Note over D: wait 100ms

    D->>LSP: hover(symbol)
    LSP-->>D: null (still not ready)
    Note over D: wait 200ms

    D->>LSP: hover(symbol)
    LSP-->>D: result ✓
    Note over D: return result

Retries use exponential backoff (100ms, 200ms, 400ms, 800ms) and apply to all operations that can return empty or null results during warmup, including hover, workspace/symbol, definition, references, and documentSymbol.

Relationship to ty

tyf is an adapter over ty’s LSP server. It does no Python analysis of its own — every definition, signature, and reference it returns comes from ty.

tyf does not ship ty

ty is a required peer dependency that you install yourself. tyf does not bundle, vendor, or pin a copy of ty. At runtime it binds to whatever ty it finds:

  1. ty on your PATH (preferred), or
  2. uvx ty as a fallback.

The analysis you get therefore depends on the ty version in your environment, not on the tyf version. Upgrading tyf does not upgrade ty, and upgrading ty does not upgrade tyf.

# Install ty yourself (required)
uv add --dev ty

Why this matters: ty is pre-release

ty is pre-release software (0.0.x) under active development, with frequent breaking changes — including to LSP and diagnostic behavior. Because tyf binds to your installed ty at runtime, a ty upgrade can change tyf’s output without any change to tyf itself.

Practical guidance:

What tyf surfaces (and what it doesn’t)

tyf surfaces ty’s navigation and symbol knowledge — definitions, type signatures, references, and class members — not ty’s type-checking diagnostics. tyf never reports type errors, so the false-positive noise that affects ty check on dynamic frameworks (Pydantic, SQLAlchemy, and similar) does not appear in tyf output.

Supported ty versions

⚠️ Placeholder — pending the version-floor task. The concrete list of supported ty versions is owned by the version-floor / CI task as a single source of truth. That source is not in this repository yet, so no version numbers are listed here (and none are invented). When the source lands, this section will reference it rather than copy it.

Support policy. tyf is tested against specific pinned ty versions and supports a contiguous range — from a documented floor up to the latest tested release. The exact floor, the latest tested version, and the policy’s bound (for example latest-N) are defined by the version-floor task referenced above.

In the meantime, you can always check which ty you are running:

ty --version
# or, if ty is only reachable through uv:
uvx ty --version

Testing

tyf’s integration tests drive a real ty lsp process — ty is never mocked. Each test spawns the actual tyf binary, which starts a real daemon and a real ty LSP server, then asserts on the structured output for known fixtures.

Fixtures live at the repo root:

FixturePurpose
example.pyMinimal single-file fixture used by the basic suite
test_project/Multi-file project exercising classes, generics, protocols, enums, async code, decorators, and exceptions
test_project2/A second workspace, used to test multi-workspace daemon behavior

The integration suites live in tests/integration/: test_basic.rs, test_complex_project.rs, test_daemon.rs, test_multi_workspace.rs, and test_project_smoke.rs.

Running the tests locally

ty must be installed and on PATH (or reachable via uvx ty). Tests that need it call require_ty() and fail fast with install instructions if it is missing.

# Install ty (required for integration tests)
uv add --dev ty

# Run everything (unit + integration)
cargo test --all-features

# Run just the basic integration suite
cargo test --test test_basic

CI runs the same cargo test --all-features suite with ty installed, plus a separate smoke-test workflow (benchmarks/smoke.sh) against the release binary.

Planned (not yet shipped): running the integration suite against each pinned ty version in the supported range, driven by the version-floor task’s source of truth.

Commands Overview

Type-aware Python code navigation (powered by ty)

Usage

tyf [OPTIONS] <COMMAND>

Global Options

--workspace
Project root (default: auto-detect)
-v, --verbose
Enable verbose output
--format
Output format: human (default), json, csv, or paths
--detail
Output detail level: condensed (token-efficient, default) or full (verbose)
--timeout
Timeout in seconds for daemon operations (default: 30)
--color
When to use colored output: auto (default), always, or never. Respects the NO_COLOR environment variable.

Exit codes

CodeMeaning
0Success — including a well-formed query that matched nothing (“not found” is a normal result, not an error)
1Runtime error (daemon unreachable, unreadable file, ty not installed)
2Usage error (malformed invocation, e.g. 2+ dots in a dotted symbol)
3The installed ty lacks a capability the command needs (see calls)

Commands

show
Definition, type signature, and usages of a symbol by name
find
Find where a symbol is defined by name (–fuzzy for partial matching)
refs
All usages of a symbol across the codebase (by name or file:line:col)
members
Public interface of a class: methods, properties, and class variables
calls
Call tree of a symbol: what it calls (--in for what calls it)
list
All functions, classes, and variables defined in a file
mcp
Serve the same commands as an MCP server over stdio (see MCP server)
daemon
Manage the background LSP server (auto-starts on first use)
guide
Agent-facing instructions: setup, then the command reference

show

Definition, type signature, and usages of a symbol — where it’s defined, its type signature, and optionally all usages. Searches the whole project by name, no file path needed.

Backward compatibility: tyf inspect still works as a hidden alias for tyf show.

Use Class.member dotted notation (one level only) to narrow to a specific class member. Module-qualified names (module.func) and nested paths (Outer.Inner.method) are not supported; using 2+ dots is a usage error.

Examples: tyf show MyClass tyf show MyClass.get_data # narrow to a specific class method tyf show calculate_sum UserService # multiple symbols at once tyf show MyClass –references # also show all usages tyf show MyClass –doc # include docstring tyf show MyClass –all # show everything: doc + refs + test refs tyf show MyClass –file src/models.py # narrow to one file

Usage

tyf show <SYMBOLS> [OPTIONS]

Arguments

<symbols> (required)
Symbol name(s) to show. Use Class.member (one level) to narrow to a class member.

Options

-f, --file
Narrow the search to a specific file (searches whole project if omitted)
-r, --references
Also find all references (can be slow on large codebases)
-d, --doc
Include the docstring in the output (off by default)
-a, --all
Show everything: docstring, references, and test references

Examples

# Show a single symbol
tyf show MyClass

# Show a specific class method (dotted notation)
tyf show MyClass.get_data

# Show multiple symbols at once
tyf show MyClass my_function

# Show a symbol in a specific file
tyf show MyClass --file src/module.py

# Include docstring
tyf show MyClass --doc

# Show everything (doc + refs + test refs)
tyf show MyClass --all

# Using the backward-compatible alias
tyf inspect MyClass

Dotted notation (Class.member)

Class.member narrows a lookup to a member of a specific class — useful when the same method name (e.g. get_data) exists on several classes. The container is resolved first, then its members are searched, so Database.get_data resolves to Database’s member and never Cache’s same-named one.

Limitations:

  • One level only. Class.member is supported; nested paths like Outer.Inner.method are not.
  • Module-qualified names are not supported. module.func will not match — dotted notation addresses class members, not module paths.
  • Two or more dots is a usage error. tyf show a.b.c prints a message to stderr and exits nonzero (it does not attempt a lookup). The same applies to a leading or trailing dot (.foo, foo.).
  • A valid dotted query that matches nothing (e.g. Database.nope or Nonesuch.get_data) is a normal “not found” and exits 0 — there is no fallback to the bare member or container name.
  • No inherited-member resolution: only members defined directly on the class are matched (same limitation as members).

Unresolved types

The Signature section reports a symbol’s type from ty’s analysis. When ty cannot resolve a type — typically because a third-party library’s stubs are not installed — it would normally report Unknown. Instead, tyf shows the literal type annotation as written in source (e.g. def build(self) -> pa.Table). Only the source file is read, so this works without the project’s dependencies installed. A symbol with no source annotation is marked (unannotated) rather than Unknown.

See also

find

Find where a function, class, or variable is defined. Searches the whole project by name — no need to know which file it’s in.

Use Class.member dotted notation (one level only) to narrow to a specific class member. Module-qualified names (module.func) and nested paths (Outer.Inner.method) are not supported; using 2+ dots is a usage error. Use --fuzzy for partial/prefix matching (returns richer symbol information including kind and container name).

Examples: tyf find calculate_sum tyf find Calculator.add # find a specific class method tyf find calculate_sum multiply divide # multiple symbols at once tyf find handler –file src/routes.py # narrow to one file tyf find handle_ –fuzzy # fuzzy/prefix match

Usage

tyf find <SYMBOLS> [OPTIONS]

Arguments

<symbols> (required)
Symbol name(s) to find. Use Class.member (one level) to narrow to a class member.

Options

-f, --file
Narrow the search to a specific file (searches whole project if omitted)
--fuzzy
Use fuzzy/prefix matching via workspace symbols (richer output with kind + container)

Examples

# Find a single symbol
tyf find calculate_sum

# Find a specific class method (dotted notation)
tyf find Calculator.add

# Find multiple symbols at once
tyf find calculate_sum multiply divide

# Find a symbol in a specific file
tyf find my_function --file src/module.py

# Fuzzy/prefix match
tyf find handle_ --fuzzy

Dotted notation (Class.member)

Class.member narrows to a member of a specific class. The container is resolved first, then its members are searched, so the same method name on multiple classes is disambiguated. With --fuzzy, the member part is matched per fuzzy/prefix rules while the container stays an exact match.

Limitations:

  • One level only — Outer.Inner.method is not supported.
  • Module-qualified names (module.func) are not supported.
  • 2+ dots is a usage error (stderr message + nonzero exit); so is a leading or trailing dot (.foo, foo.).
  • A valid dotted query matching nothing exits 0 (normal “not found”) with no fallback to the bare member or container.

See also

refs

All usages of a symbol across the codebase. Useful before renaming or removing code to understand the impact.

Use Class.member dotted notation (one level only) to narrow to a specific class member. Module-qualified names (module.func) and nested paths (Outer.Inner.method) are not supported; using 2+ dots is a usage error.

Usage

# Position mode (exact)
tyf refs -f <FILE> -l <LINE> -c <COLUMN>

# Symbol mode (parallel search)
tyf refs <QUERIES>... [-f <FILE>]

# Stdin mode (pipe positions or symbol names)
... | tyf refs --stdin

Arguments

ArgumentDescription
<QUERIES>...Symbol names or file:line:col positions (auto-detected)

Options

OptionDescription
-f, --fileFile path (required for position mode, optional for symbol mode)
-l, --lineLine number (position mode, requires –file and –column)
-c, --columnColumn number (position mode, requires –file and –line)
--stdinRead queries from stdin (one per line)
--include-declarationInclude the declaration in the results

Examples

# Position mode: exact location
tyf refs -f main.py -l 10 -c 5

# Symbol mode: find references by name
tyf refs my_function

# Symbol mode: dotted notation for a specific method
tyf refs Calculator.add

# Symbol mode: multiple symbols searched in parallel
tyf refs my_function MyClass calculate_sum

# Auto-detected file:line:col positions (parallel)
tyf refs main.py:10:5 utils.py:20:3

# Mixed: positions and symbols together
tyf refs main.py:10:5 my_function

# Pipe from list
tyf list file.py --format csv \
  | awk -F, 'NR>1{printf "file.py:%s:%s\n",$3,$4}' \
  | tyf refs --stdin

# Pipe symbol names
tyf list file.py --format csv \
  | tail -n+2 | cut -d, -f1 \
  | tyf refs --stdin

Dotted notation (Class.member)

In symbol mode, Class.member narrows references to a member of a specific class (the container is resolved first, then its members), disambiguating a method name that exists on several classes.

Limitations:

  • One level only — Outer.Inner.method is not supported.
  • Module-qualified names (module.func) are not supported.
  • 2+ dots is a usage error (stderr message + nonzero exit); so is a leading or trailing dot (.foo, foo.). file:line:col positions are unaffected — dots in a file path are not treated as dotted notation.
  • A valid dotted query matching nothing exits 0 (normal “not found”) with no fallback.

See also

members

Public interface of a class – methods with signatures, properties, and class variables with types. Like ‘list’ scoped to a class, with type info included.

Excludes private (_prefixed) and dunder (dunder) members by default; use –all to include everything.

Note: only shows members defined directly on the class, not inherited members.

Examples: tyf members MyClass tyf members MyClass UserService # multiple classes tyf members MyClass –all # include init, repr, etc tyf members MyClass -f src/models.py # narrow to one file

Usage

tyf members <SYMBOLS> [OPTIONS]

Arguments

<symbols> (required)
Class name(s) to show (supports multiple classes)

Options

-f, --file
Narrow the search to a specific file (searches whole project if omitted)
--all
Include dunder methods and private members (excluded by default)

Examples

# Show public interface of a class
tyf members MyClass

# Multiple classes at once
tyf members MyClass UserService

# Include dunder methods and private members
tyf members MyClass --all

# Narrow to a specific file
tyf members MyClass --file src/models.py

Output format

The default text output groups members by category:

MyClass (src/models.py:15:1)
  Methods:
    calculate_total(self, items: list[Item]) -> Decimal    :42:5
    validate(self) -> bool                                 :58:5
  Properties:
    name: str                                              :20:5
    is_active: bool                                        :23:5
  Class variables:
    MAX_RETRIES: int = 3                                   :16:5

Line/col references on the right allow jumping to the source.

Unresolved types

When ty cannot resolve a type — most often because a third-party library’s stubs are not installed in the analyzed environment — it would normally report the type as Unknown. Instead of surfacing that, tyf substitutes the literal type annotation as written in the source file:

Workshop (src/workshop.py:4:1)
  Methods:
    build(self) -> pa.Table                                :7:5     # source said `-> pa.Table`
  Class variables:
    default_table: pa.Table                                :5:5

This works without the analyzed project’s dependencies or stubs installed — only the source file is read. If a member genuinely has no annotation in source, it is marked (unannotated) rather than Unknown.

Limitations

  • Only shows members defined directly on the class, not inherited members (MRO traversal is not yet supported by ty’s LSP)
  • Type signatures come from hover, so they require ty to have analyzed the file
  • When a type cannot be resolved, the literal source annotation is shown in place of Unknown; (unannotated) marks a member with no source annotation

See also

calls

Call tree of a symbol – recursively, as a tree. Outgoing (the default) answers “what does this do, transitively”; incoming (--in) answers “who calls this”.

Outgoing replaces a chain of file reads when following a code path. Incoming complements refs for impact analysis: refs gives raw text ranges, calls --in gives the calling function’s identity, one level per hop.

Use Class.member dotted notation (one level only) to narrow to a specific class member. Module-qualified names (module.func) and nested paths (Outer.Inner.method) are not supported; using 2+ dots is a usage error.

Requires a ty with call-hierarchy support (ty 0.0.41 or newer). On an older ty the command exits 3 with a message naming the installed version — a distinct code from “symbol not found” (which exits 0) and from a usage error (2).

Examples: tyf calls process_order # what it calls, 2 levels deep tyf calls process_order –depth 3 # deeper tyf calls check_inventory –in # who calls it tyf calls OrderPipeline.run # a specific class method tyf calls a b c # several symbols at once tyf calls process_order –external # locate stdlib callees too

Usage

tyf calls <SYMBOLS> [OPTIONS]

Arguments

<symbols> (required)
Symbol name(s). Use Class.member (one level) to narrow to a class member

Options

--in
Incoming calls: who calls this symbol. Mutually exclusive with --out
--out
Outgoing calls: what this symbol calls. The explicit form of the default
--depth
Recursion depth (default 2, maximum 5; larger values are clamped, not rejected)
--external
Show locations for out-of-workspace callees. They are still never expanded
-f, --file
Narrow the initial symbol lookup to a specific file

Examples

# What does this function do, transitively?
tyf calls process_order

# Go deeper
tyf calls process_order --depth 3

# Who calls this? (impact analysis before an edit)
tyf calls check_inventory --in

# A specific class method
tyf calls OrderPipeline.run

# Several symbols in one invocation
tyf calls process_order charge_payment validate_order

# Locate stdlib/site-packages callees (still not expanded)
tyf calls check_inventory --external

# Add the call-site line numbers
tyf --detail full calls process_order

# Machine-readable
tyf --format json calls process_order
tyf --format csv calls process_order

Output format

The default is a condensed tree, two spaces of indent per level:

process_order (src/orders.py:41:1)
  validate_order (src/orders.py:88:1)
    check_inventory (src/inventory.py:12:1)
  charge_payment (src/payments.py:30:1)
    check_inventory ↑
  log.info (external)

Markers:

MarkerMeaning
↑Already expanded elsewhere in this tree; the subtree is shown at its first occurrence
(cycle)Re-enters a definition already on the path from the root (direct or mutual recursion)
(external)Defined outside the workspace (stdlib, site-packages); never expanded

Incoming (--in) uses the same tree shape — each level is one caller hop:

check_inventory (src/inventory.py:12:1)
  validate_order (src/orders.py:88:1)
    process_order (src/orders.py:41:1)
  charge_payment (src/payments.py:30:1)
    process_order ↑

--detail full appends the call sites from the LSP fromRanges:

process_order (src/orders.py:41:1)
  validate_order (src/orders.py:88:1) [called at 43]

--format json mirrors the tree with the markers as explicit booleans (cycle, deduped, external), so a consumer never parses the glyphs. --format csv flattens it to symbol,depth,name,file,line,column,flag rows.

A symbol that resolves but has no calls is a normal result, not an error: it prints (no outgoing calls) and exits 0.

Why the tree is pruned

The command exists to cost fewer tokens than reading each file in the chain, so the walk suppresses repetition rather than reproducing it:

  • Dedup (↑) — a callee reached from two parents (a diamond) is expanded at its first occurrence only. Without this, a shared helper’s whole subtree repeats once per caller.
  • Cycle ((cycle)) — recursion terminates instead of running to the depth cap. Direct (f calls f) and mutual (a calls b calls a) are both detected, by definition identity rather than by name.
  • Depth — capped at 5 in the daemon regardless of what is requested.

Dotted notation (Class.member)

Class.member narrows to a member of a specific class (the container is resolved first, then its members), disambiguating a method name that exists on several classes. Semantics and limits are identical to show, find, and refs:

  • One level only — Outer.Inner.method is not supported.
  • Module-qualified names (module.func) are not supported.
  • 2+ dots is a usage error (stderr message + nonzero exit); so is a leading or trailing dot (.foo, foo.).
  • A valid dotted query matching nothing exits 0 (normal “not found”) with no fallback.

Limitations

  • Calls through callbacks and dynamic dispatch are invisible. A function passed as an argument and invoked by the callee, a method resolved at runtime, a getattr call — static analysis cannot see these, so they do not appear in either direction. tyf does not paper over this with heuristics or text search: an absent edge means ty could not prove one.
  • External code is never expanded, under any flag. --external adds the location of a stdlib or site-packages callee; it never walks into it.
  • Depth is capped at 5. --depth 99 behaves as --depth 5 and is not an error.
  • A decorator application counts as an outgoing call. A decorated function lists its decorator alongside the callees in its body.
  • A class has no calls. Construction is not modelled as a call to the class, so tyf calls MyClass reports no calls. Query a method instead (tyf calls MyClass.method).
  • Results depend on the installed ty. Call hierarchy requires ty 0.0.41 or newer; behavior can change between ty releases. The observed behavior this command is built on is recorded in docs/dev/call-hierarchy-spike.md.

See also

  • Commands Overview
  • refs – all usages as raw locations, without caller identity
  • show – definition, type, and usages of any symbol

list

All functions, classes, and variables defined in a file — like a table of contents for your code.

Examples: tyf list src/services/user.py

Usage

tyf list <FILE>

Arguments

<file> (required)

Examples

# List all symbols in a file
tyf list main.py

See also

mcp

Serve the same commands as an MCP server over stdio, for harnesses that consume MCP servers more easily than a shell command.

Exposes one tool per lookup command — show, find, refs, members, list — with the same parameters, the same dotted-notation rules, and byte-identical condensed output. calls is CLI-only for now. It is a thin bridge over the same background daemon the CLI uses, so both frontends share one warm index.

Runs until stdin closes; the harness starts and stops it. stdio is the only transport. The workspace is resolved once at startup from --workspace, or auto-detected from the process’s working directory when that is omitted.

See MCP server for registration snippets, the full tool surface, and how MCP compares to the CLI.

Usage

tyf mcp [OPTIONS]

Options

--workspace <PATH>
Project root for every tool call (default: auto-detect from the working directory)

Examples

# Workspace auto-detected from the current directory — what a harness normally spawns
tyf mcp

# Explicit workspace, for a harness that spawns servers elsewhere
tyf mcp --workspace /path/to/project

See also

daemon

Manage the background LSP server (auto-starts on first use)

Usage

tyf daemon

Subcommands

start
Start the background LSP server
stop
Stop the background LSP server
restart
Stop and restart the background LSP server
status
Show the daemon’s running status

Examples

# Start the background daemon
tyf daemon start

# Restart the daemon (e.g. after upgrading tyf)
tyf daemon restart

# Check daemon status
tyf daemon status

# Stop the daemon
tyf daemon stop

See also

guide

Agent-facing instructions, embedded in the binary so they always match the build. Prints one page and exits; never touches the workspace, the daemon or the LSP, so it is safe as the very first command in a fresh checkout — including uvx ty-find guide, which needs nothing installed.

With no page given, picks one from the working directory: use when the nearest pyproject.toml has a .venv with tyf installed, setup otherwise. Both pages are always reachable by name.

The pages live in docs/guide/ and are compiled into the binary with include_str!, so the text always matches the build. Each page is under 60 lines, plain ASCII, and ends with a single next: line telling the reader what to run.

Usage

tyf guide [PAGE]

Arguments

<page>
Page to print: setup or use (default: detect from the working directory)

Examples

uvx ty-find guide      # from anywhere, nothing installed
tyf guide              # auto-select
tyf guide setup        # install, wire into CLAUDE.md, run a first query
tyf guide use          # command reference: which command answers which question

See also

MCP server

tyf mcp serves the same symbol lookups as the CLI, as an MCP server over stdio.

Use MCP where your harness makes MCP easier than shell. The CLI remains the primary interface for Claude Code — Claude Code already runs shell commands well, and tyf show MyClass costs less context than a tool schema injected into every session. Reach for the MCP server when your harness consumes MCP servers more readily than it shells out.

Both frontends talk to the same background daemon, so a warm index is shared between them. Running the MCP server does not give up anything the CLI offers, and it does not duplicate any state.

Transport

stdio only. The harness spawns tyf mcp, writes JSON-RPC on its stdin and reads it on its stdout. There is no HTTP transport — tyf is a local tool.

The server runs until stdin closes, then exits. It does not stop the background daemon on exit; the daemon’s idle timeout retires it, exactly as after CLI use.

The protocol revision is 2026-07-28.

Workspace resolution

The workspace is resolved once, at startup, and used for every tool call:

  1. tyf mcp --workspace <path> — explicit, wins if present.
  2. Otherwise it is auto-detected from the working directory of the tyf mcp process, using the same walk-up-for-project-markers logic as the CLI’s --workspace default: the nearest ancestor with a pyproject.toml (or similar marker) wins, falling back to the working directory itself when there is none. Harnesses spawn MCP servers in the project directory, so this is the normal path and usually needs no configuration.

MCP roots are deprecated in the 2026-07-28 revision and are not used.

One server instance serves one workspace. A harness working across several repositories spawns one server per repository.

Registering the server

Claude Code

claude mcp add tyf -- tyf mcp

Or in .mcp.json at the project root:

{
  "mcpServers": {
    "tyf": {
      "command": "tyf",
      "args": ["mcp"]
    }
  }
}

Codex

In ~/.codex/config.toml:

[mcp_servers.tyf]
command = "tyf"
args = ["mcp"]

Cursor

In .cursor/mcp.json (project) or ~/.cursor/mcp.json (global):

{
  "mcpServers": {
    "tyf": {
      "command": "tyf",
      "args": ["mcp"]
    }
  }
}

Gemini CLI

In .gemini/settings.json (project) or ~/.gemini/settings.json (global):

{
  "mcpServers": {
    "tyf": {
      "command": "tyf",
      "args": ["mcp"]
    }
  }
}

If the harness does not spawn the server in your project directory, add the workspace explicitly:

{
  "mcpServers": {
    "tyf": {
      "command": "tyf",
      "args": ["mcp", "--workspace", "/path/to/project"]
    }
  }
}

Tools

One tool per lookup command, with the same parameters and the same semantics. calls has no tool yet — it is gated on ty 0.0.41 or newer, so it stays CLI-only until that floor is the norm.

ToolParametersCLI equivalent
showsymbols (required), file, references, doc, alltyf show
findsymbols (required), file, fuzzytyf find
refsqueries (required), file, include_declarationtyf refs
memberssymbols (required), file, alltyf members
listfile (required)tyf list

Every symbol and query parameter is an array — pass several symbols in one call rather than one call per symbol.

refs accepts symbol names and file:line:col positions in the same array, auto-detected per entry, just like the CLI.

Three differences from the CLI are worth knowing:

  • refs’s include_declaration defaults to false here; the CLI’s --include-declaration defaults to true. The tool schema states the default, so an agent reading it sees the same thing you do.
  • show’s test references follow all — there is no separate tests parameter.
  • There is no references_limit parameter: show and refs display at most 20 individual references, the CLI’s default. Output that hits the cap carries the CLI’s “use --references-limit 0 to show all” hint, which an MCP caller cannot act on — narrow the query instead, or use the CLI.

file parameters are resolved against the workspace root, not the server’s working directory. Absolute paths work too.

Dotted notation

Class.member (one level) narrows to a specific class member, exactly as on the CLI:

{ "symbols": ["Calculator.add"] }

Module-qualified names (module.func) and nested paths (Outer.Inner.method) are not supported. A token with 2+ dots, a leading dot, or a trailing dot is a tool error (isError: true) carrying the same message the CLI prints. A well-formed query that simply matches nothing is a normal success result with the “not found” text — the same distinction the CLI draws between exit 2 and exit 0.

Output

Tool results are text only — the same condensed, uncoloured output the CLI produces, with trailing whitespace trimmed. There is no structuredContent and no result outputSchema: one format, one renderer, shared with the CLI.

Startup and errors

The background daemon auto-starts on the first tool call, including the restart-on-version-mismatch path, exactly as it does for the CLI. The first call after a cold start pays the index warmup; subsequent calls are fast.

A harness may pipeline tool calls; each is served on its own task. Daemon startup is serialized within the process, so concurrent first calls wait for one daemon rather than each spawning their own.

Errors that reach the bridge — an unreachable daemon, a missing ty — come back as tool errors with the same message the CLI would print. See Troubleshooting.

Performance

ty-find uses a background daemon to keep the ty LSP server running between calls. The first call starts the daemon and waits for LSP initialization, which is slower. Subsequent calls reuse the running server and respond quickly.

Benchmarks

Benchmarks are run as part of CI. See tests/bench_* for the test code.

OperationFirst call (cold daemon)Subsequent calls (warm)
showTBDTBD
findTBDTBD
refsTBDTBD

These numbers will be filled in with real measurements from the benchmark suite.

What affects performance

  • Project size: Larger Python projects take longer for the initial LSP indexing.
  • Daemon state: Cold starts include daemon spawn + LSP initialization. Warm calls skip both.
  • Disk I/O: First call after a file change triggers re-indexing by the LSP server.

Troubleshooting

“ty: command not found”

ty-find requires ty to be installed and on PATH. Install it with:

uv add --dev ty

If ty is installed but not on PATH, ty-find will attempt to run it via uvx ty as a fallback. If neither works, you’ll see this error.

Daemon won’t start

Check the daemon status:

tyf daemon status

For more detail, enable debug logging:

RUST_LOG=ty_find=debug tyf daemon start

Common causes:

  • Another process is using the daemon’s socket.
  • ty is not installed (see above).
  • Permissions issue on the socket file.

Wrong or stale results

If tyf returns outdated definitions or missing references, the LSP server may have stale state. Restart the daemon:

tyf daemon stop && tyf daemon start

Then retry your query.

Slow first call

The first call in a session is expected to be slower because it:

  1. Starts the background daemon process.
  2. Spawns the ty LSP server.
  3. Waits for LSP initialization and project indexing.

Subsequent calls reuse the running daemon and typically respond in 50–100ms. If every call is slow, check that the daemon is staying alive between calls with tyf daemon status.

Empty results for something that should exist

Empty output has two distinct causes — diagnose which one you’re hitting before assuming a tyf bug.

(a) The symbol isn’t statically visible. Some dynamic constructs are invisible to static analysis: getattr/setattr access, runtime-generated classes and methods, attributes assigned through __init__ magic, and names created by metaprogramming. If the symbol only exists at runtime, ty can’t see it and neither can tyf. First, verify the symbol is in a .py file within the workspace.

(b) ty couldn’t resolve the environment. If ty is pointed at the wrong interpreter, is missing type stubs, or a third-party package isn’t installed, it fails to resolve imports — and unresolved imports cascade into empty navigation results. This looks identical to “not found” but is an environment problem, not a tyf problem.

Diagnostic: run ty check on the file directly:

ty check path/to/file.py

If ty reports unresolved imports (e.g. “Cannot resolve imported module”), it’s an environment problem — fix the interpreter/stubs/dependencies, not your tyf query. See how tyf behaves with third-party packages and .venv resolution in How It Works. If imports resolve cleanly but the symbol still doesn’t appear, you’re in case (a) above.

Which ty am I running, and is it supported?

Because tyf binds at runtime to whatever ty is on your PATH (or uvx ty), the ty version determines your results. Print it with:

ty --version
# or, if ty is only reachable through uv:
uvx ty --version

Compare that against the Supported ty versions list. ty is pre-release (0.0.x) and changes often, so if results shift after an upgrade, a version outside the supported range is the first thing to check.

Signatures show a source annotation or (unannotated)

This is expected behavior, not a bug. When ty cannot resolve a type — most often because a third-party library’s stubs aren’t installed — it would normally report Unknown. Instead, tyf falls back to the literal type annotation as written in the source file (e.g. -> pa.Table), reading only the source so it works without the project’s dependencies installed. A symbol with no source annotation is shown as (unannotated) rather than Unknown.

See the “Unresolved types” sections on the show and members pages for details.

Debug logging

For any issue, enable full debug output:

RUST_LOG=ty_find=debug tyf show MySymbol

This shows the LSP messages exchanged with ty, which helps diagnose protocol-level issues.