python-docs-mcp-server 0.1.6__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,7 @@ import sqlite3
10
10
  from dataclasses import dataclass, field
11
11
  from pathlib import Path
12
12
 
13
+ from mcp_server_python_docs.services.compare import CompareService
13
14
  from mcp_server_python_docs.services.content import ContentService
14
15
  from mcp_server_python_docs.services.package_docs import PackageDocsService
15
16
  from mcp_server_python_docs.services.persistent_cache import PersistentDocsCache
@@ -25,6 +26,7 @@ class AppContext:
25
26
  index_path: Path
26
27
  search_service: SearchService
27
28
  content_service: ContentService
29
+ compare_service: CompareService
28
30
  version_service: VersionService
29
31
  package_docs_service: PackageDocsService = field(default_factory=PackageDocsService)
30
32
  persistent_docs_cache: PersistentDocsCache | None = None
@@ -214,3 +214,90 @@ class PackageDocsResult(BaseModel):
214
214
  default=None,
215
215
  description="Controlled-scope note, for example skipped labels or not-found details",
216
216
  )
217
+
218
+
219
+ # --- compare_versions models ---
220
+
221
+ ChangeKind = Literal["added", "removed", "changed", "unchanged"]
222
+
223
+
224
+ class CompareVersionsResult(BaseModel):
225
+ """Output from compare_versions tool (CMPR-01, CMPR-03).
226
+
227
+ Typed contract returned by CompareService.compare and the source FastMCP
228
+ auto-derives the compare_versions outputSchema from. The four ``change``
229
+ cases (added / removed / changed / unchanged) all share this shape;
230
+ optional delta fields are ``None`` when they do not apply to the case,
231
+ keeping the JSON shape predictable and token-frugal (no full re-printing
232
+ of unchanged content, per CMPR-03).
233
+ """
234
+
235
+ symbol: str = Field(description="Qualified symbol name being compared")
236
+ v1: str = Field(description="Source Python version")
237
+ v2: str = Field(description="Target Python version")
238
+ change: ChangeKind = Field(
239
+ description=(
240
+ "Diff discriminator: 'added' (symbol new in v2), 'removed' "
241
+ "(symbol absent in v2), 'changed' (symbol present in both with a "
242
+ "detected delta), or 'unchanged' (symbol present in both, no delta)"
243
+ )
244
+ )
245
+
246
+ new_in: str | None = Field(
247
+ default=None,
248
+ description=(
249
+ "Version string extracted from the v2 section text; populated when "
250
+ "change == 'added', and may also be set when change == 'changed' if "
251
+ "the v2 section carries a versionadded marker for a sub-feature"
252
+ ),
253
+ )
254
+ removed_in: str | None = Field(
255
+ default=None,
256
+ description=(
257
+ "Version where the symbol is first absent; populated when "
258
+ "change == 'removed' (equals v2)"
259
+ ),
260
+ )
261
+ changed_in: str | None = Field(
262
+ default=None,
263
+ description=(
264
+ "Version extracted from the v2 section; populated when "
265
+ "change == 'changed'"
266
+ ),
267
+ )
268
+ deprecated_in: str | None = Field(
269
+ default=None,
270
+ description="Version extracted from a deprecation marker in the v2 section",
271
+ )
272
+ signature_delta: str | None = Field(
273
+ default=None,
274
+ description=(
275
+ "Best-effort heuristic: first non-empty diff line between v1 and v2 "
276
+ "section text. MAY be a docstring change or prose change rather than "
277
+ "a true signature change — treat as advisory, not authoritative."
278
+ ),
279
+ )
280
+ see_also_added: list[str] = Field(
281
+ default_factory=list,
282
+ description="See-also link labels present in v2 but not v1",
283
+ )
284
+ see_also_removed: list[str] = Field(
285
+ default_factory=list,
286
+ description="See-also link labels present in v1 but not v2",
287
+ )
288
+ section_diff: str | None = Field(
289
+ default=None,
290
+ description=(
291
+ "Short unified-diff snippet (difflib.unified_diff), truncated to "
292
+ "honor the token budget; populated only when change == 'changed' and "
293
+ "the diff is non-trivially short"
294
+ ),
295
+ )
296
+ note: str | None = Field(
297
+ default=None,
298
+ description=(
299
+ "Optional advisory note about result completeness, e.g. when docs "
300
+ "pages could not be fetched for one or both versions and the diff is "
301
+ "therefore based on symbol presence alone."
302
+ ),
303
+ )
@@ -28,12 +28,14 @@ from mcp_server_python_docs.app_context import AppContext
28
28
  from mcp_server_python_docs.detection import detect_python_version, match_to_indexed
29
29
  from mcp_server_python_docs.errors import DocsServerError
30
30
  from mcp_server_python_docs.models import (
31
+ CompareVersionsResult,
31
32
  DetectPythonVersionResult,
32
33
  GetDocsResult,
33
34
  ListVersionsResult,
34
35
  PackageDocsResult,
35
36
  SearchDocsResult,
36
37
  )
38
+ from mcp_server_python_docs.services.compare import CompareService
37
39
  from mcp_server_python_docs.services.content import ContentService
38
40
  from mcp_server_python_docs.services.package_docs import PackageDocsService
39
41
  from mcp_server_python_docs.services.persistent_cache import PersistentDocsCache
@@ -158,6 +160,7 @@ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
158
160
  )
159
161
  search_svc = SearchService(db, synonyms)
160
162
  content_svc = ContentService(db, persistent_cache=persistent_docs_cache)
163
+ compare_svc = CompareService(db, content_svc)
161
164
  version_svc = VersionService(db)
162
165
  package_docs_svc = PackageDocsService()
163
166
 
@@ -183,6 +186,7 @@ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
183
186
  synonyms=synonyms,
184
187
  search_service=search_svc,
185
188
  content_service=content_svc,
189
+ compare_service=compare_svc,
186
190
  version_service=version_svc,
187
191
  package_docs_service=package_docs_svc,
188
192
  persistent_docs_cache=persistent_docs_cache,
@@ -268,6 +272,18 @@ PackageParam = Annotated[
268
272
  str,
269
273
  Field(min_length=1, max_length=214, description="PyPI package/project name"),
270
274
  ]
275
+ SymbolParam = Annotated[
276
+ str,
277
+ Field(
278
+ min_length=1,
279
+ max_length=200,
280
+ description="Qualified Python symbol name, e.g. 'asyncio.TaskGroup'",
281
+ ),
282
+ ]
283
+ CompareVersionParam = Annotated[
284
+ str,
285
+ Field(description="Python version string, e.g. '3.11'"),
286
+ ]
271
287
 
272
288
 
273
289
  def create_server() -> FastMCP:
@@ -374,6 +390,27 @@ def create_server() -> FastMCP:
374
390
  is_default=detected_ver is not None,
375
391
  )
376
392
 
393
+ @mcp.tool(annotations=_TOOL_ANNOTATIONS)
394
+ def compare_versions(
395
+ symbol: SymbolParam,
396
+ v1: CompareVersionParam,
397
+ v2: CompareVersionParam,
398
+ ctx: Context = None, # type: ignore[assignment]
399
+ ) -> CompareVersionsResult:
400
+ """Diff a Python stdlib symbol between two indexed versions. Returns
401
+ change=added|removed|changed|unchanged with optional new_in, changed_in,
402
+ deprecated_in, signature_delta (advisory), see_also_added/removed,
403
+ section_diff, and note fields. Both versions must be indexed; otherwise
404
+ an actionable error names the available versions."""
405
+ app_ctx: AppContext = ctx.request_context.lifespan_context
406
+ try:
407
+ return app_ctx.compare_service.compare(symbol, v1, v2)
408
+ except DocsServerError as e:
409
+ raise ToolError(str(e))
410
+ except Exception as e:
411
+ logger.exception("Unexpected error in compare_versions")
412
+ raise ToolError(f"Internal error: {type(e).__name__}")
413
+
377
414
  # SRVR-07: _meta hint for get_docs tool.
378
415
  # FastMCP 1.27 does not expose a public API for setting _meta on tool
379
416
  # definitions. Deferred until the mcp SDK adds _meta support to the
@@ -0,0 +1,303 @@
1
+ """Version-diff service for the compare_versions MCP tool (CMPR-01/02/03).
2
+
3
+ Phase 09. Implements ``CompareService.compare(symbol, v1, v2)`` returning a
4
+ ``CompareVersionsResult``: the four diff branches (added / removed / changed /
5
+ unchanged), the version/symbol error paths, and the see-also / deprecation /
6
+ signature-delta heuristics.
7
+
8
+ No structured signature/deprecation/see-also metadata is stored in the index,
9
+ so this service derives those signals from section *text* using the regex
10
+ literals locked by the Plan 09-01 data-shape spike, plus symbol presence/absence
11
+ across the two doc_sets (via ``create_symbol_cache``).
12
+
13
+ Algorithm ordering is FIXED per cross-AI review H2:
14
+ 1. validate BOTH versions (validate_version raises the unknown-version error
15
+ from version_resolution; compare.py never names that exception type — H4)
16
+ 2. resolve the symbol in BOTH versions
17
+ 3. if missing in BOTH -> SymbolNotFoundError
18
+ 4. ONLY THEN handle the v1 == v2 identical-versions case as 'unchanged'
19
+
20
+ This ordering ensures ``compare('does.not.exist', '3.11', '3.11')`` raises
21
+ SymbolNotFoundError rather than falsely returning 'unchanged'.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import difflib
26
+ import re
27
+ import sqlite3
28
+ from typing import TYPE_CHECKING
29
+
30
+ from mcp_server_python_docs.errors import PageNotFoundError, SymbolNotFoundError
31
+ from mcp_server_python_docs.models import CompareVersionsResult
32
+ from mcp_server_python_docs.services.cache import create_symbol_cache
33
+ from mcp_server_python_docs.services.observability import log_tool_call
34
+ from mcp_server_python_docs.services.version_resolution import validate_version
35
+
36
+ if TYPE_CHECKING:
37
+ from mcp_server_python_docs.services.content import ContentService
38
+
39
+ # --- Locked extractor regexes (verbatim from 09-01-data-shape-spike-SUMMARY) ---
40
+ # All four HOLD against the spike fixture (A1/A2/sibling probes). Scalar
41
+ # extractors return None on no-match; _extract_see_also returns [].
42
+ _NEW_IN_RE = re.compile(r"New in version\s+(\d+\.\d+)")
43
+ _CHANGED_IN_RE = re.compile(r"Changed in version\s+(\d+\.\d+)")
44
+ _DEPRECATED_IN_RE = re.compile(r"Deprecated since version\s+(\d+\.\d+)")
45
+ # Markdown link label extractor; MUST be applied only within a "See also" window
46
+ # (locate case-insensitive "see also", read forward to next ATX heading / window
47
+ # end), not against the whole section, or it captures unrelated body links.
48
+ _SEE_ALSO_LINK_RE = re.compile(r"\[([^\]]+)\]\(")
49
+
50
+ # M2 note text emitted when a docs page is unfetchable in the both-present branch.
51
+ _PAGE_UNAVAILABLE_NOTE = "docs page not available for one or both versions"
52
+
53
+ # section_diff truncation ceiling (token-frugality, CMPR-03).
54
+ _SECTION_DIFF_MAX_CHARS = 600
55
+ # signature_delta heuristic: first-line snippet length cap.
56
+ _SIGNATURE_LINE_MAX = 80
57
+
58
+
59
+ def _extract_version(pattern: re.Pattern[str], text: str) -> str | None:
60
+ """Return the captured version from the first match of ``pattern``, or None.
61
+
62
+ Shared by the New-in / Changed-in / Deprecated-since extractors, which differ
63
+ only by their (spike-locked) pattern literal (``_NEW_IN_RE`` etc.).
64
+ """
65
+ match = pattern.search(text)
66
+ return match.group(1) if match else None
67
+
68
+
69
+ def _extract_see_also(text: str) -> list[str]:
70
+ """Extract see-also link labels from the 'See also' window only.
71
+
72
+ Locates the first case-insensitive 'see also' line, then reads forward to the
73
+ next ATX heading ('#') or up to 20 lines, and matches markdown link labels in
74
+ that window. Returns [] when there is no 'See also' section (fallback policy).
75
+ """
76
+ lines = text.splitlines()
77
+ start = None
78
+ for idx, line in enumerate(lines):
79
+ if "see also" in line.lower():
80
+ start = idx
81
+ break
82
+ if start is None:
83
+ return []
84
+
85
+ # WR-01: a Sphinx "See also" admonition is one contiguous block. markdownify
86
+ # rarely emits an ATX heading after it, so an ATX-only break over-captures
87
+ # unrelated body links. Skip leading blanks, then read the block until the
88
+ # first blank line AFTER content has started (or an ATX heading, or the cap).
89
+ window: list[str] = []
90
+ started = False
91
+ for line in lines[start + 1 : start + 1 + 20]:
92
+ if line.lstrip().startswith("#"):
93
+ break
94
+ if line.strip():
95
+ started = True
96
+ window.append(line)
97
+ elif started:
98
+ break # blank line ends the admonition block
99
+
100
+ return _SEE_ALSO_LINK_RE.findall("\n".join(window))
101
+
102
+
103
+ def _first_nonempty_line(text: str) -> str:
104
+ """Return the first non-blank line of a section text, or '' if none."""
105
+ for line in text.splitlines():
106
+ if line.strip():
107
+ return line.strip()
108
+ return ""
109
+
110
+
111
+ class CompareService:
112
+ """Compute a structured version diff for a stdlib symbol (CMPR-01).
113
+
114
+ Composes existing primitives: ``validate_version`` (version existence +
115
+ actionable error), ``create_symbol_cache`` (version-scoped symbol
116
+ resolution), and ``ContentService.get_docs`` (section text retrieval).
117
+ The method is intentionally SYNC — SQLite reads on the open connection are
118
+ non-blocking; no asyncio.to_thread is used.
119
+ """
120
+
121
+ def __init__(
122
+ self,
123
+ db: sqlite3.Connection,
124
+ content_service: ContentService,
125
+ ) -> None:
126
+ self._db = db
127
+ self._content = content_service
128
+ # Private per-service LRU symbol resolver (own cache, RESEARCH §Q7).
129
+ self._resolve = create_symbol_cache(db)
130
+
131
+ def _section_text(self, uri: str, anchor: str, version: str) -> str:
132
+ """Fetch the section text for a symbol via ContentService.
133
+
134
+ Derives the slug from the symbol URI (everything before '#') and uses the
135
+ symbol anchor — never joins symbols.section_id directly (RESEARCH Pitfall 2).
136
+
137
+ CR-01: symbol URIs carry ".html" (``library/json.html#...``) but real
138
+ Sphinx ingestion stores ``documents.slug`` EXTENSIONLESS
139
+ (``library/json``), and ``get_docs`` matches ``documents.slug`` exactly.
140
+ Try the extensionless form first, then the raw ".html" page, mirroring
141
+ ``retrieval.ranker._document_candidates`` so resolution holds on a real
142
+ index. Raises PageNotFoundError only when every candidate misses.
143
+ """
144
+ page = uri.split("#", 1)[0]
145
+ candidates = (page[:-5], page) if page.endswith(".html") else (page,)
146
+ last_exc: PageNotFoundError | None = None
147
+ for slug in candidates:
148
+ try:
149
+ result = self._content.get_docs(
150
+ slug=slug, version=version, anchor=anchor
151
+ )
152
+ except PageNotFoundError as exc:
153
+ last_exc = exc
154
+ continue
155
+ return result.content
156
+ # candidates is non-empty, so last_exc is always set when we reach here.
157
+ assert last_exc is not None
158
+ raise last_exc
159
+
160
+ @log_tool_call("compare_versions")
161
+ def compare(self, symbol: str, v1: str, v2: str) -> CompareVersionsResult:
162
+ """Compare a symbol's documentation between two Python versions.
163
+
164
+ See module docstring for the FIXED branch ordering (H2). Returns one of
165
+ the four ``change`` cases; validate_version raises the unknown-version
166
+ error from its own module, or raises SymbolNotFoundError when the symbol
167
+ is absent in both versions.
168
+ """
169
+ # 1. Validate BOTH versions first. validate_version raises the
170
+ # unknown-version error (with the indexed-version list) from its own
171
+ # module — compare.py never references that exception type (H4).
172
+ validate_version(self._db, v1)
173
+ validate_version(self._db, v2)
174
+
175
+ # 2. Resolve the symbol in BOTH versions.
176
+ sym_v1 = self._resolve(symbol, v1)
177
+ sym_v2 = self._resolve(symbol, v2)
178
+
179
+ # 3. Missing in BOTH versions -> SymbolNotFoundError. This fires BEFORE
180
+ # the v1 == v2 check so identical-versions with a non-existent symbol
181
+ # raises rather than returning 'unchanged' (H2 fix).
182
+ if sym_v1 is None and sym_v2 is None:
183
+ raise SymbolNotFoundError(
184
+ f"symbol {symbol!r} not found in v{v1} or v{v2}"
185
+ )
186
+
187
+ # 4. Identical versions, symbol known to exist -> 'unchanged'.
188
+ if v1 == v2:
189
+ return CompareVersionsResult(
190
+ symbol=symbol, v1=v1, v2=v2, change="unchanged"
191
+ )
192
+
193
+ # 5. Branch on symbol presence across the two versions.
194
+ if sym_v1 is None and sym_v2 is not None:
195
+ # Added in v2. Best-effort new_in extraction; swallow
196
+ # PageNotFoundError (structural 'added' is sound without the section).
197
+ new_in: str | None = None
198
+ try:
199
+ text_v2 = self._section_text(sym_v2.uri, sym_v2.anchor, v2)
200
+ new_in = _extract_version(_NEW_IN_RE, text_v2)
201
+ except PageNotFoundError:
202
+ new_in = None
203
+ return CompareVersionsResult(
204
+ symbol=symbol, v1=v1, v2=v2, change="added", new_in=new_in
205
+ )
206
+
207
+ if sym_v1 is not None and sym_v2 is None:
208
+ # Removed in v2 — no section fetch needed.
209
+ return CompareVersionsResult(
210
+ symbol=symbol, v1=v1, v2=v2, change="removed", removed_in=v2
211
+ )
212
+
213
+ # Both present. Narrow for the type checker.
214
+ assert sym_v1 is not None and sym_v2 is not None
215
+
216
+ # M2: if either section is unfetchable, report 'changed' + note rather
217
+ # than the previous false-negative 'unchanged'.
218
+ try:
219
+ text_v1 = self._section_text(sym_v1.uri, sym_v1.anchor, v1)
220
+ text_v2 = self._section_text(sym_v2.uri, sym_v2.anchor, v2)
221
+ except PageNotFoundError:
222
+ return CompareVersionsResult(
223
+ symbol=symbol,
224
+ v1=v1,
225
+ v2=v2,
226
+ change="changed",
227
+ section_diff=None,
228
+ note=_PAGE_UNAVAILABLE_NOTE,
229
+ )
230
+
231
+ changed_in = _extract_version(_CHANGED_IN_RE, text_v2)
232
+ deprecated_in = _extract_version(_DEPRECATED_IN_RE, text_v2)
233
+
234
+ # signature_delta (M1, advisory): first non-empty line comparison.
235
+ first_v1 = _first_nonempty_line(text_v1)
236
+ first_v2 = _first_nonempty_line(text_v2)
237
+ signature_delta: str | None = None
238
+ if first_v1 != first_v2:
239
+ signature_delta = (
240
+ f"line 1 differs (v{v1}: {first_v1[:_SIGNATURE_LINE_MAX]} "
241
+ f"-> v{v2}: {first_v2[:_SIGNATURE_LINE_MAX]})"
242
+ )
243
+
244
+ # See-also delta (set difference of link labels in each "See also" window).
245
+ see_v1 = set(_extract_see_also(text_v1))
246
+ see_v2 = set(_extract_see_also(text_v2))
247
+ see_also_added = sorted(see_v2 - see_v1)
248
+ see_also_removed = sorted(see_v1 - see_v2)
249
+
250
+ # Unified diff of the two section texts, truncated for token frugality.
251
+ diff_lines = list(
252
+ difflib.unified_diff(
253
+ text_v1.splitlines(),
254
+ text_v2.splitlines(),
255
+ lineterm="",
256
+ n=2,
257
+ )
258
+ )
259
+ # WR-02: truncate on LINE boundaries (not mid-line) with an explicit
260
+ # marker, so the emitted diff stays a parseable unified diff.
261
+ section_diff: str | None = None
262
+ if diff_lines:
263
+ kept: list[str] = []
264
+ used = 0
265
+ truncated = False
266
+ for line in diff_lines:
267
+ projected = used + len(line) + (1 if kept else 0)
268
+ if projected > _SECTION_DIFF_MAX_CHARS:
269
+ truncated = True
270
+ break
271
+ kept.append(line)
272
+ used = projected
273
+ section_diff = "\n".join(kept)
274
+ if truncated:
275
+ marker = "... (diff truncated)"
276
+ section_diff = f"{section_diff}\n{marker}" if section_diff else marker
277
+
278
+ # If nothing changed at all, the symbol is genuinely 'unchanged'.
279
+ has_delta = (
280
+ section_diff is not None
281
+ or changed_in is not None
282
+ or deprecated_in is not None
283
+ or signature_delta is not None
284
+ or bool(see_also_added)
285
+ or bool(see_also_removed)
286
+ )
287
+ if not has_delta:
288
+ return CompareVersionsResult(
289
+ symbol=symbol, v1=v1, v2=v2, change="unchanged"
290
+ )
291
+
292
+ return CompareVersionsResult(
293
+ symbol=symbol,
294
+ v1=v1,
295
+ v2=v2,
296
+ change="changed",
297
+ changed_in=changed_in,
298
+ deprecated_in=deprecated_in,
299
+ signature_delta=signature_delta,
300
+ see_also_added=see_also_added,
301
+ see_also_removed=see_also_removed,
302
+ section_diff=section_diff,
303
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-docs-mcp-server
3
- Version: 0.1.6
3
+ Version: 0.2.0
4
4
  Summary: The canonical Python stdlib oracle for AI coding agents — exact symbols, exact sections, exact versions, offline, always free, always MIT, token-frugal.
5
5
  Author-email: Aymen Hammouda <hammouda.aymen@gmail.com>
6
6
  License-Expression: MIT
@@ -71,7 +71,7 @@ tools for lookup and section retrieval.
71
71
  - page and section retrieval with truncation and pagination
72
72
  - a local SQLite + FTS5 index; no runtime web scraping
73
73
  - results for each Python version you index
74
- - five read-only MCP tools
74
+ - six read-only MCP tools
75
75
 
76
76
  ## Quick example
77
77
 
@@ -205,7 +205,7 @@ Contributor commands and validation steps live in
205
205
 
206
206
  ## Tools
207
207
 
208
- The server currently exposes five MCP tools:
208
+ The server currently exposes six MCP tools:
209
209
 
210
210
  | Tool | Description |
211
211
  |------|-------------|
@@ -214,6 +214,7 @@ The server currently exposes five MCP tools:
214
214
  | `lookup_package_docs` | Look up official PyPI package metadata and return package-declared documentation/homepage/source URLs. This is a controlled PyPI metadata lookup, not generic web search. |
215
215
  | `list_versions` | List all indexed Python versions with metadata. |
216
216
  | `detect_python_version` | Detect the user's local Python version and report whether that version has been indexed. |
217
+ | `compare_versions` | Diff a Python stdlib symbol between two indexed versions. Returns `change=added|removed|changed|unchanged` with optional `new_in`, `changed_in`, `deprecated_in`, `signature_delta` (advisory heuristic), `see_also_added/removed`, `section_diff`, and `note` deltas. Token-frugal — emits only changed fields, not full content. |
217
218
 
218
219
  ## Why not Context7 or generic docs retrieval?
219
220
 
@@ -1,12 +1,12 @@
1
1
  mcp_server_python_docs/__init__.py,sha256=sUtQT81momSAPMywIkgnR7BZxJHn53rpcjSE3CTAUws,499
2
2
  mcp_server_python_docs/__main__.py,sha256=_LmCa1p3O1gSnOcOC7UXoNbqmIo2LBha3FDsfUUkJ_8,21196
3
- mcp_server_python_docs/app_context.py,sha256=0HpzQjKjFcbgYhZUvJYuZw9F3xbAckvfYCMKQlWHfZo,1300
3
+ mcp_server_python_docs/app_context.py,sha256=5kK4JcTiKgJGOV_gFE8aVktYdtCpeDbZunIrlt4SxyA,1403
4
4
  mcp_server_python_docs/detection.py,sha256=bYYSfpMymHk9wehNW66oItX6yFUoCC2JXDRZLYpM0Vw,2860
5
5
  mcp_server_python_docs/diagnostics.py,sha256=FyC6QDFLvozARpYuMnaKfJwaN6BCzVycME_SkNs2YIs,2370
6
6
  mcp_server_python_docs/errors.py,sha256=XTOm1raJfxS2adaOPZdwj0SuhpMAXAdjBsF2-zTv_1E,827
7
- mcp_server_python_docs/models.py,sha256=jtC1TEAOZHBDEfFf8uqsSzC5FKmY06zRrHWzw3HSmsg,6966
7
+ mcp_server_python_docs/models.py,sha256=A9XtcJPJFDa6jn7pHa4yDS1TPlnpIc3A7WEeKu4qqOw,10247
8
8
  mcp_server_python_docs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- mcp_server_python_docs/server.py,sha256=ZsGLMkOlI3ok3LrF2hWUZhgW_5d_lolA7jFcIRfMrCY,13976
9
+ mcp_server_python_docs/server.py,sha256=8F27o2hTyi1rwSNoMo8lGYXCvhwEHS7cVHI7n9p5Y2M,15469
10
10
  mcp_server_python_docs/data/synonyms.yaml,sha256=LgJxamZ9QT6Pykva7M1uM8juSd9ybHuv3vUOKrUFMg4,10696
11
11
  mcp_server_python_docs/ingestion/__init__.py,sha256=Yd7Jk7hD7XG3eoBVQpYiG94EwJGfI3qXxVCpA8JszYg,66
12
12
  mcp_server_python_docs/ingestion/cpython_versions.py,sha256=mEhjuT7q8Bo1aZ-zj6PSqmbq2Bz64g4xj50ttqA4QUU,977
@@ -19,6 +19,7 @@ mcp_server_python_docs/retrieval/query.py,sha256=9rWBvWQG3KwVjCqEqCYKMFgKqswU-O4
19
19
  mcp_server_python_docs/retrieval/ranker.py,sha256=GSssXzfaJKEWrCgF_UuTm9-lSzTbYe3cSalsqgq0nUI,11712
20
20
  mcp_server_python_docs/services/__init__.py,sha256=UKlaRQYo_cnLOUVf_QieXnHlcnH9ejEa3pYgHhJiG1E,320
21
21
  mcp_server_python_docs/services/cache.py,sha256=ADqg89johOJRYZysbes1bEs6nLFrmzS9L5c-Njt1mrU,3199
22
+ mcp_server_python_docs/services/compare.py,sha256=wWsgD8w9fl3L7gr-mcdd6ntY_V1pGtZ-ues1iyyNRPg,12743
22
23
  mcp_server_python_docs/services/content.py,sha256=7O5XGbR9QWICMxYAd-cp_pPF86tNLcYAMIwyCP_i8dk,5342
23
24
  mcp_server_python_docs/services/observability.py,sha256=rwnkIDelNkclHzuwsktGUoU2fzYCuiR1niVRmOwJHJg,4839
24
25
  mcp_server_python_docs/services/package_docs.py,sha256=0Qq8PtRL_KdiTFUtllaj4KFHz6u9L903zcORVpYmR1U,5605
@@ -29,8 +30,8 @@ mcp_server_python_docs/services/version_resolution.py,sha256=rULBoqq97t0ZfSWSy-e
29
30
  mcp_server_python_docs/storage/__init__.py,sha256=YNeNr7FW9Vgvo79oXgKXE3eGP0WwWDqRgTZ7lg7MFW4,62
30
31
  mcp_server_python_docs/storage/db.py,sha256=Lys8HN3IvgmNpr_qyocUQghcZnwHf2lz9eq_BFmwi54,5634
31
32
  mcp_server_python_docs/storage/schema.sql,sha256=4cXPktPQSLfbjkSjirpf0dA2CphlO9UCIKUxnLiIm_g,5637
32
- python_docs_mcp_server-0.1.6.dist-info/METADATA,sha256=k-EWmidaKAmIMJElpLcQo-LNh93g0zFAS9xejQJ4jaY,13532
33
- python_docs_mcp_server-0.1.6.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
34
- python_docs_mcp_server-0.1.6.dist-info/entry_points.txt,sha256=-1arbtjZhF5mtnJNAh_90UBr1f4AqJxT3Tmfy9p7NMI,142
35
- python_docs_mcp_server-0.1.6.dist-info/licenses/LICENSE,sha256=-AHGWRwcvoeP4bkY1W2ok_qaI1Mg0Zx2qYgR1k-3CHY,1071
36
- python_docs_mcp_server-0.1.6.dist-info/RECORD,,
33
+ python_docs_mcp_server-0.2.0.dist-info/METADATA,sha256=Yb2uC9uMwyceWUJiUxa-Da1TRIKLkpqhu6TXCHJHKWM,13880
34
+ python_docs_mcp_server-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
35
+ python_docs_mcp_server-0.2.0.dist-info/entry_points.txt,sha256=-1arbtjZhF5mtnJNAh_90UBr1f4AqJxT3Tmfy9p7NMI,142
36
+ python_docs_mcp_server-0.2.0.dist-info/licenses/LICENSE,sha256=-AHGWRwcvoeP4bkY1W2ok_qaI1Mg0Zx2qYgR1k-3CHY,1071
37
+ python_docs_mcp_server-0.2.0.dist-info/RECORD,,