A Model Context Protocol server for semantic code analysis, providing tools to query code structure, annotations, diagnostics, dead code detection, and semantic diffs across C# project snapshots.
Lurp MCP server demonstrates solid definition quality with comprehensive tool schemas and detailed descriptions. All 10 tools have explicit input schemas with typed parameters and descriptions. Tool names follow verb_noun convention (get_, list_, find_, search_). Descriptions are detailed (100-500+ chars) and explain intent, parameters, and pagination behavior. However, several tools lack output schema documentation in the visible code, and error handling guidance is minimal. Parameter descriptions are thorough but some tools expose complex internal concepts (docCommentId|assemblyIdentity format, snapshot_id semantics) that could confuse LLMs. No tool annotations (readOnlyHint/destructiveHint) visible despite ReadOnly/destructive metadata in attributes.
Assemble a context capsule for a symbol or file location. Exactly one of symbol or file+line is required. Intent: inspect | modify | diagnose (default: inspect). Supports tier continuation via tier/cursor.
List dead-code candidates for the pinned snapshot: symbols of kind Type, Method, Property, Field, Event with no incoming LIVE edge (Calls, Constructs, Reads, Writes, Handles, RoutesTo, Registers, MapsTo, MayDispatchTo, StaticallyCalls, TestedBy, ReflectionTypeRef, ReflectionMemberRef, ReflectionNameCandidate) of strong provenance (compiler_proved, framework_derived, global_implementation_relation). Suppression ladder: (1) strong LIVE edge -> alive, not dead; (2) MayDispatchTo with possible provenance -> uncertain_dead reason possible_dispatch; (3) convention/name_candidate/runtime_unknown edges -> uncertain_dead with matching DeclaredBoundaries text (framework_convention, name_candidate, runtime_unknown); (4) candidate overlaps binding_incompleteness UnobservableReasons -> unresolved reason binding_incompleteness; (5) candidate is the compilation's own process entry point (explicit static Main or the compiler-synthesized top-level-statements form) -> uncertain_dead reason entry_point_convention, checked ahead of the public-surface step below since entry points have no in-repo caller by definition regardless of accessibility; (6) Public/Protected without --include-public -> excluded from proved_dead, with include_public -> uncertain_dead reason public_surface; (7) EF private member of MapsTo entity or attribute-free serialization-eligible public/internal property in System.Text.Json-referencing project -> uncertain_dead reason ef_convention or serialization_convention. Default excludes generated symbols and test-project symbols (project name %.Tests); lift with include_generated/include_tests (surfaced as uncertain reason generated_excluded/test_harness). Batched WHERE target_symbol_id IN (...) per page, never per-candidate GetIncomingEdges. Keyset pagination via limit/cursor ordered by symbol_id ASC; limit default 50 max 200. Filters: project (assembly name), document (forward-slash relative path), kind (Type/Method/Property/Field/Event). Response carries snapshot_id, filters, incoming_edge_kinds_checked[14], live_provenance_rank[3], uncertain_provenance[4], candidate_count (total filtered universe), dead_count/uncertain_count/unresolved_count (totals across all pages), candidates[] page window with symbol_id, fqn, kind, accessibility, document_path, locations[], project_name, declaration_count, is_generated, status (proved_dead/uncertain_dead/unresolved/uncertain), reason, uncertainties[] (verbatim UncertaintyDetector/DeclaredBoundaries wording), incoming_edge_summary, plus next_cursor and freshness.
Output schemas not documented in visible code. Tools return JSON envelopes but LLMs cannot see field definitions, types, or required fields. Breaks downstream tool chaining and forces LLMs to infer structure.
Tool annotations missing. ReadOnly and destructive metadata exist in C# attributes but are not exposed as MCP tool annotations (readOnlyHint, destructiveHint). LLMs cannot distinguish safe reads from destructive writes without explicit hints.
Complex symbol resolution formats (docCommentId|assemblyIdentity, bare T: docCommentId, FQN) exposed in parameter descriptions. LLMs will struggle to construct valid symbol identifiers without examples or a dedicated resolution tool.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | C | 62 | 2025-06-18+ | v2 |
List diagnostics captured at index time for the pinned snapshot: compiler diagnostics plus any diagnostics from analyzers referenced by the target project (e.g. built-in SDK analyzers like CA1822). IDE-only code-style rules (e.g. IDE0005) are not included even when the target project's own build enables them (e.g. EnforceCodeStyleInBuild plus an .editorconfig severity) — Roslyn's build-time compilation additionally gates IDE0005 behind GenerateDocumentationFile=true, independent of anything Lurp does. For unused-using-directive detection, query the compiler diagnostic instead: severity=hidden, id=CS8019 ("Unnecessary using directive") — Lurp always captures this regardless of the target's analyzer/code-style configuration. Filters: document (solution-relative path, not git-relative), project, severity (Hidden, Info, Warning, Error case-insensitive, or "all" for every severity including Hidden; default excludes Hidden), id (diagnostic code, e.g. CS8933), include_generated (default false: excludes diagnostics located in obj/**, *.g.cs, *.generated.cs, *.Designer.cs, *ModelSnapshot.cs — the same generated-file patterns every other read tool's include_generated excludes by default). Unknown severity values are rejected with an error instead of returning empty. Keyset pagination via limit/cursor, ordered by diagnostic_id. diagnostic_count is the total match count across all pages, not the page size. start_column/end_column are 0-based (unlike the 1-based start_line/end_line). Reports what the compiler and analyzers said at index time — not re-evaluated, ranked, or deduplicated.
Compute semantic diff between two snapshots. Both from_snapshot and to_snapshot are required.
Resolve a symbol in the pinned snapshot. Accepts pipe-form docCommentId|assemblyIdentity, bare T: docCommentId, or bare FQN.
Get annotations in the pinned snapshot. Supports three modes: by symbol (resolved via docCommentId|assemblyIdentity, bare T: docCommentId, or FQN), by document (git-relative forward-slashed path), or whole-snapshot when neither is given. Optional kind filter and keyset pagination via limit/cursor (default 100, ordered by annotation_id). annotation_count is the total match count across all pages, not the page size — use next_cursor for pagination. Annotations with document_path IS NULL (user-authored via lurp_annotate) are unreachable via document filter.
Fetch source text for a document in the pinned snapshot. Supports line windowing: start_line/end_line are 1-based inclusive; context_lines expands symmetrically. When no window is given the whole file is returned. Pass outline=true to include the declaration outline in the same response.
Get symbol metadata and optional source. Accepts pipe-form docCommentId|assemblyIdentity, bare T: docCommentId, or bare FQN. View summary|source|all controls payload.
Literal/exact-text search over source content in the pinned snapshot. Returns per-occurrence results with 1-based start_line/end_line and 0-based start_column/end_column plus the full line_text. Use this instead of lurp_search when you need the exact line where a string appears. Supports pagination via cursor and case-insensitive search via ignore_case. match_count is the total match count across all pages, not the page size — use next_cursor for pagination.
Retract (hard-delete) one annotation by annotation_id, scoped to the pinned snapshot — any provenance, not limited to user-authored (lurp_annotate) rows; extractor-derived rows can be removed the same way. The annotation_id is the surrogate PK from lurp_get_annotations. The delete is WHERE snapshot_id=@pinned AND annotation_id=@id — one row only, no cross-snapshot effect. Copy-forward clones allocate fresh ids, so retracting in snapshot A does not affect snapshot B's clone.
Error handling lacks recovery guidance. Code throws McpProtocolException with messages like 'cursor is not a valid continuation token' but does not suggest next steps (e.g., 'omit cursor to start from page 1').
Snapshot_id parameter semantics unclear. Description says 'uses pinned if not provided' but LLMs cannot infer what 'pinned' means or how to discover available snapshots. No discovery tool visible.