mcpbeat

Python Lsp

oaustegard/claude-python-lsp

Semantic Python code queries via a stdio LSP client driving pyright-langserver. Provides binding-resolved go-to-definition, find-references, hover types, type diagnostics, file symbol outlines, and project-wide symbol search — name resolution and type inference that tree-sitter and ripgrep cannot do. Use when you need to follow an import to a definition, find all real uses of a symbol (excluding same-named-but-unrelated ones), get an inferred type, surface type errors, outline a file, or search symbols across a project. Triggers on "go to definition", "find references", "what type is", "resolve this symbol", "symbol outline", "find symbol in project", "pyright", "type-check this file".

This is a copy. The original lives at oaustegard/python-lsp.

13k tokens
context cost
the whole folder, loaded on every use
10
files
ships runnable scripts
0
copies elsewhere
how many repositories repackaged it
137
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/oaustegard/claude-skills --skill python-lsp

The instruction itself

7 sections, as written by the author

python-lsp

A thin, dependency-free Python client that owns the LSP lifecycle against

pyright-langserver --stdio and exposes high-value semantic queries.

Why, over tree-sitter / ripgrep: tree-sitter gives a CST — structural

queries, call-site enumeration by name. It cannot do name resolution, type

inference, or cross-file binding. ripgrep matches text, so it false-positives

on shadowed / same-named symbols. pyright resolves bindings. This client is

that semantic overlay.

Setup (self-installing)

The client bootstraps pyright on first use. Run the bootstrap explicitly, or

let LSPClient do it via ensure_pyright():

sh /mnt/skills/user/python-lsp/scripts/bootstrap.sh
# or, equivalently, the one-liner it wraps:
command -v pyright-langserver >/dev/null || uv tool install pyright

pyright wheels vendor the langserver JS bundle and run it on system node

no npm install, no separate fetch when node is present. Measured cold (caches

wiped): uv tool install pyright ~0.7s, first working server ~1.8s total; warm

sub-second.

Node prerequisite. The clean path assumes system node (v18+) is present.

With no node, pyright-python falls back to downloading node from nodejs.org,

which may be blocked in locked-down containers. The bootstrap detects node and

fails loudly (exit 1, clear message) rather than hanging.

Usage: CLI

LSP=/mnt/skills/user/python-lsp/scripts/lsp_client.py

python3 $LSP bootstrap                              # ensure pyright installed
python3 $LSP <root> definition  <file> <line> <col>
python3 $LSP <root> references  <file> <line> <col>
python3 $LSP <root> hover       <file> <line> <col>
python3 $LSP <root> diagnostics <file>
python3 $LSP <root> symbols     <file>             # documentSymbol outline
python3 $LSP <root> wsymbols    <query>            # workspace/symbol search

Positions are zero-based line/character (LSP spec). <file> is relative to

<root> or absolute.

Usage: library

The scripts/ module lands on the boot .pth, so it is importable directly.

import sys; sys.path.insert(0, "/mnt/skills/user/python-lsp/scripts")
from lsp_client import LSPClient

with LSPClient("/path/to/repo") as c:        # context manager reaps the subprocess
    c.open_all("pkg/service.py", "pkg/models.py")
    c.wait_for_index()                       # REQUIRED before querying — see below
    defs  = c.definition("pkg/service.py", 4, 8)   # -> [Location], follows imports
    refs  = c.references("pkg/models.py", 8, 4)     # -> [Location], binding-resolved
    typ   = c.hover("pkg/service.py", 4, 4)         # -> "(variable) u: User"
    diags = c.diagnostics("pkg/bad.py")             # -> [diagnostic dicts]
    outln = c.document_symbols("pkg/models.py")     # -> [SymbolInfo], file outline
    hits  = c.workspace_symbols("User")             # -> [SymbolInfo], project-wide

Location has .path, .start_line, .start_char, .end_line, .end_char

(all zero-based) and .as_dict(). Convert 1-based UI input with

Position.from_one_based(line, col).

The root you pass is split into two roles. Relative <file> arguments resolve

against it (call that the *scope*), but pyright is rooted at the enclosing

project root — by default LSPClient climbs out of any package the scope

sits inside (every ancestor with an __init__.py) via find_project_root. This

is what makes a query scoped to a sub-package (scipy/optimize) still resolve

the project's own absolute imports (from scipy.optimize._x import Y); without

it, references silently undercount — same blindness as a text grep, opposite

direction. The promotion is announced on stderr. Pass auto_root=False to pin

pyright at the scope verbatim.

Methods

| Method | Returns | Notes |

|---|---|---|

| definition(file, line, col) | list[Location] | Go-to-definition across files/imports. |

| references(file, line, col) | list[Location] | Binding-resolved — the win over ripgrep. Excludes same-named, unrelated symbols. |

| hover(file, line, col) | str \| None | Inferred type / signature string. |

| diagnostics(file) | list[dict] | pyright type/error diagnostics for the file. |

| document_symbols(file) | list[SymbolInfo] | One file's outline (classes/functions/methods); nesting via .container. |

| workspace_symbols(query) | list[SymbolInfo] | Project-wide fuzzy symbol search. Empty query = every symbol (expensive). |

SymbolInfo has .name, .kind (int), .kind_name (e.g. "Class"), .location (a Location), .container, and .as_dict().

The lifecycle gotchas (the parts that bite)

  • Wait for indexing before querying. Querying mid-index returns empty

results — the most common silent failure. wait_for_index() blocks on

pyright's $/progress begin/end cycle (with a diagnostics-arrival fallback).

Always call it after did_open / open_all and before any query.

  • Don't advertise workspace.configuration or workspace.workspaceFolders.

If the client claims either capability, pyright defers *all* analysis until

the corresponding negotiation completes — the server starts its service

instance and then goes silent (no diagnostics, no progress, queries hang).

start() advertises neither, so pyright uses its defaults and analyzes open

files immediately — no didChangeConfiguration nudge needed. Relevant if you

reimplement the lifecycle or add capabilities. (Bisected against the fixture;

workspace.symbol is safe to advertise.)

  • Reap the subprocess. Use the context manager (or call stop()) so

sessions don't leak pyright-langserver processes. stop() sends

shutdown + exit, then waits/terminates/kills as needed.

  • Open the files you query. Queries auto-did_open their target file, but

for cross-file references open all relevant files first so pyright has

built their models.

  • Root at the project, not the sub-package. pyright resolves absolute

intra-project imports only when rooted where the top-level package is

importable. Rooted at a sub-package, those imports fail and references

undercount with no error. LSPClient auto-detects this (climbs out of the

enclosing package; see find_project_root) and prints the promotion to

stderr; auto_root=False opts out. This was a live silent-undercount bug:

--refs ScalarFunction over scipy/optimize returned 3 references rooted at

the sub-package vs 30 rooted at the project root.

Tests

cd /mnt/skills/user/python-lsp
python3 -m pytest tests/test_lsp_client.py -v

Round-trips against tests/fixture/ (a small multi-file package): definition

follows an import, references excludes an unrelated same-named symbol, hover

returns an inferred type, diagnostics flags an intentional type error, the

indexing-wait is verified deterministic, and subprocess cleanup is checked for

orphans.

How to use it

Copy the folder

Take oaustegard/claude-python-lsp from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.

Install what it needs

The instructions reference npm. Without those the skill loads but fails at the first command.