python-docs-mcp-server 0.3.0__py3-none-any.whl → 0.3.2__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.
- mcp_server_python_docs/ingestion/sphinx_json.py +26 -1
- mcp_server_python_docs/retrieval/ranker.py +56 -6
- mcp_server_python_docs/services/content.py +3 -24
- mcp_server_python_docs/services/search.py +64 -3
- {python_docs_mcp_server-0.3.0.dist-info → python_docs_mcp_server-0.3.2.dist-info}/METADATA +54 -3
- {python_docs_mcp_server-0.3.0.dist-info → python_docs_mcp_server-0.3.2.dist-info}/RECORD +9 -9
- {python_docs_mcp_server-0.3.0.dist-info → python_docs_mcp_server-0.3.2.dist-info}/WHEEL +1 -1
- {python_docs_mcp_server-0.3.0.dist-info → python_docs_mcp_server-0.3.2.dist-info}/entry_points.txt +0 -0
- {python_docs_mcp_server-0.3.0.dist-info → python_docs_mcp_server-0.3.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -287,9 +287,16 @@ def extract_sections(body_html: str, doc_uri: str) -> list[dict]:
|
|
|
287
287
|
return []
|
|
288
288
|
|
|
289
289
|
soup = BeautifulSoup(body_html, "html.parser")
|
|
290
|
+
# Modern Sphinx puts the ID on a section; only its first direct heading names it.
|
|
291
|
+
for section in soup.find_all(["section", "div"], id=True):
|
|
292
|
+
if section.name != "section" and "section" not in (section.get("class") or []):
|
|
293
|
+
continue
|
|
294
|
+
heading = section.find(re.compile(r"^h[1-6]$"), recursive=False)
|
|
295
|
+
if isinstance(heading, Tag) and not heading.get("id"):
|
|
296
|
+
heading["id"] = section["id"]
|
|
290
297
|
heading_tags = soup.find_all(re.compile(r"^h[1-6]$"), id=True)
|
|
291
298
|
|
|
292
|
-
if not heading_tags:
|
|
299
|
+
if not heading_tags and not soup.find("dt", id=True):
|
|
293
300
|
# No headings with id — create a single section covering the whole body
|
|
294
301
|
content = html_to_markdown(body_html)
|
|
295
302
|
if not content:
|
|
@@ -346,6 +353,24 @@ def extract_sections(body_html: str, doc_uri: str) -> list[dict]:
|
|
|
346
353
|
}
|
|
347
354
|
)
|
|
348
355
|
|
|
356
|
+
# Sphinx API signatures carry independent anchors inside definition lists.
|
|
357
|
+
anchors = {section["anchor"] for section in sections}
|
|
358
|
+
for signature in soup.find_all("dt", id=True):
|
|
359
|
+
anchor = str(signature["id"])
|
|
360
|
+
if anchor in anchors:
|
|
361
|
+
continue
|
|
362
|
+
anchors.add(anchor)
|
|
363
|
+
description = signature.find_next_sibling("dd")
|
|
364
|
+
content = html_to_markdown(str(signature) + (str(description) if description else ""))
|
|
365
|
+
sections.append({
|
|
366
|
+
"anchor": anchor,
|
|
367
|
+
"heading": signature.get_text(" ", strip=True).rstrip("¶").strip(),
|
|
368
|
+
"level": 3,
|
|
369
|
+
"ordinal": len(sections),
|
|
370
|
+
"content_text": content,
|
|
371
|
+
"char_count": len(content),
|
|
372
|
+
"uri": f"{doc_uri}#{anchor}",
|
|
373
|
+
})
|
|
349
374
|
return sections
|
|
350
375
|
|
|
351
376
|
|
|
@@ -9,7 +9,9 @@ Receives sqlite3.Connection as parameter -- does not import storage.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import logging
|
|
12
|
+
import re
|
|
12
13
|
import sqlite3
|
|
14
|
+
from difflib import SequenceMatcher
|
|
13
15
|
|
|
14
16
|
from mcp_server_python_docs.models import SymbolHit
|
|
15
17
|
|
|
@@ -133,6 +135,32 @@ def _normalize_scores(hits: list[SymbolHit]) -> list[SymbolHit]:
|
|
|
133
135
|
return normalized
|
|
134
136
|
|
|
135
137
|
|
|
138
|
+
def _section_excerpt_overlaps(first: sqlite3.Row, second: sqlite3.Row) -> bool:
|
|
139
|
+
"""Only collapse nested sections when their *matched excerpts* are redundant.
|
|
140
|
+
|
|
141
|
+
A shared page is not enough: an overview can independently answer a broad
|
|
142
|
+
query while a nearby API section answers a precise one. The content and the
|
|
143
|
+
FTS snippet must both overlap substantially before they compete for a slot.
|
|
144
|
+
"""
|
|
145
|
+
if first["version"] != second["version"] or first["slug"] != second["slug"]:
|
|
146
|
+
return False
|
|
147
|
+
first_content = " ".join(first["content_text"].casefold().split())
|
|
148
|
+
second_content = " ".join(second["content_text"].casefold().split())
|
|
149
|
+
if not first_content or not second_content:
|
|
150
|
+
return False
|
|
151
|
+
if first_content not in second_content and second_content not in first_content:
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
first_words = re.findall(r"\w+", first["snippet_text"].casefold())
|
|
155
|
+
second_words = re.findall(r"\w+", second["snippet_text"].casefold())
|
|
156
|
+
shorter = min(len(first_words), len(second_words))
|
|
157
|
+
if shorter < 8:
|
|
158
|
+
return False
|
|
159
|
+
shared = SequenceMatcher(None, first_words, second_words, autojunk=False)
|
|
160
|
+
match = shared.find_longest_match(0, len(first_words), 0, len(second_words))
|
|
161
|
+
return match.size >= max(8, (7 * shorter + 9) // 10)
|
|
162
|
+
|
|
163
|
+
|
|
136
164
|
def search_sections(
|
|
137
165
|
conn: sqlite3.Connection,
|
|
138
166
|
match_expr: str,
|
|
@@ -154,10 +182,15 @@ def search_sections(
|
|
|
154
182
|
Returns:
|
|
155
183
|
List of SymbolHit with kind="section" and FTS5 snippets.
|
|
156
184
|
"""
|
|
185
|
+
# Inspect a fixed, ranked window even after the result slots fill: a later
|
|
186
|
+
# narrow API anchor can replace an earlier enclosing overview. The server
|
|
187
|
+
# caps max_results at 20, so this examines at most 160 candidates and does
|
|
188
|
+
# not paginate through an unbounded run of overlapping sections.
|
|
189
|
+
candidate_limit = max(40, max_results * 8)
|
|
157
190
|
try:
|
|
158
|
-
|
|
191
|
+
candidates = conn.execute(
|
|
159
192
|
"""
|
|
160
|
-
SELECT s.id, s.heading, s.uri, s.anchor,
|
|
193
|
+
SELECT s.id, s.heading, s.uri, s.anchor, s.content_text,
|
|
161
194
|
d.version, doc.slug,
|
|
162
195
|
bm25(sections_fts, 10.0, 1.0) as score,
|
|
163
196
|
snippet(sections_fts, 1, '**', '**', '...', 32) as snippet_text
|
|
@@ -167,16 +200,33 @@ def search_sections(
|
|
|
167
200
|
JOIN doc_sets d ON doc.doc_set_id = d.id
|
|
168
201
|
WHERE sections_fts MATCH ?
|
|
169
202
|
AND (? IS NULL OR d.version = ?)
|
|
170
|
-
ORDER BY bm25(sections_fts, 10.0, 1.0)
|
|
203
|
+
ORDER BY bm25(sections_fts, 10.0, 1.0), s.id
|
|
171
204
|
LIMIT ?
|
|
172
205
|
""",
|
|
173
|
-
(match_expr, version, version,
|
|
174
|
-
)
|
|
175
|
-
rows = cursor.fetchall()
|
|
206
|
+
(match_expr, version, version, candidate_limit),
|
|
207
|
+
).fetchall()
|
|
176
208
|
except sqlite3.OperationalError:
|
|
177
209
|
logger.warning("FTS5 query failed for sections: %r", match_expr)
|
|
178
210
|
return []
|
|
179
211
|
|
|
212
|
+
selected: list[sqlite3.Row] = []
|
|
213
|
+
for row in candidates:
|
|
214
|
+
duplicate = next(
|
|
215
|
+
(i for i, prior in enumerate(selected)
|
|
216
|
+
if _section_excerpt_overlaps(prior, row)),
|
|
217
|
+
None,
|
|
218
|
+
)
|
|
219
|
+
if duplicate is None:
|
|
220
|
+
selected.append(row)
|
|
221
|
+
elif len(row["content_text"]) < len(selected[duplicate]["content_text"]):
|
|
222
|
+
# The narrower section gives a more precise canonical anchor
|
|
223
|
+
# for the same matched text. The overview stays available
|
|
224
|
+
# through its anchor and whole-page get_docs retrieval.
|
|
225
|
+
selected[duplicate] = row
|
|
226
|
+
|
|
227
|
+
selected.sort(key=lambda row: (row["score"], row["id"]))
|
|
228
|
+
rows = selected[:max_results]
|
|
229
|
+
|
|
180
230
|
hits = [
|
|
181
231
|
SymbolHit(
|
|
182
232
|
uri=row["uri"],
|
|
@@ -67,7 +67,7 @@ class ContentService:
|
|
|
67
67
|
# Find the document
|
|
68
68
|
doc_row = self._db.execute(
|
|
69
69
|
"""
|
|
70
|
-
SELECT d.id, d.title, d.slug
|
|
70
|
+
SELECT d.id, d.title, d.slug, d.content_text
|
|
71
71
|
FROM documents d
|
|
72
72
|
JOIN doc_sets ds ON d.doc_set_id = ds.id
|
|
73
73
|
WHERE d.slug = ? AND ds.version = ?
|
|
@@ -105,29 +105,8 @@ class ContentService:
|
|
|
105
105
|
f"Section {anchor!r} not found in {slug!r} v{resolved_version}"
|
|
106
106
|
)
|
|
107
107
|
else:
|
|
108
|
-
#
|
|
109
|
-
|
|
110
|
-
"""
|
|
111
|
-
SELECT heading, content_text
|
|
112
|
-
FROM sections
|
|
113
|
-
WHERE document_id = ?
|
|
114
|
-
ORDER BY ordinal
|
|
115
|
-
""",
|
|
116
|
-
(doc_id,),
|
|
117
|
-
).fetchall()
|
|
118
|
-
|
|
119
|
-
if not section_rows:
|
|
120
|
-
full_text = ""
|
|
121
|
-
else:
|
|
122
|
-
parts = []
|
|
123
|
-
for row in section_rows:
|
|
124
|
-
heading = row["heading"] or ""
|
|
125
|
-
content = row["content_text"] or ""
|
|
126
|
-
if heading:
|
|
127
|
-
parts.append(f"## {heading}\n\n{content}")
|
|
128
|
-
else:
|
|
129
|
-
parts.append(content)
|
|
130
|
-
full_text = "\n\n".join(parts)
|
|
108
|
+
# The stored page is canonical; nested anchor excerpts can overlap.
|
|
109
|
+
full_text = doc_row["content_text"] or ""
|
|
131
110
|
|
|
132
111
|
title = doc_title
|
|
133
112
|
|
|
@@ -6,9 +6,11 @@ No MCP types imported — dependency rule enforced.
|
|
|
6
6
|
"""
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import re
|
|
9
10
|
import sqlite3
|
|
11
|
+
from urllib.parse import urlsplit
|
|
10
12
|
|
|
11
|
-
from mcp_server_python_docs.models import SearchDocsResult
|
|
13
|
+
from mcp_server_python_docs.models import SearchDocsResult, SymbolHit
|
|
12
14
|
from mcp_server_python_docs.retrieval.query import (
|
|
13
15
|
build_match_expression,
|
|
14
16
|
classify_query,
|
|
@@ -23,6 +25,8 @@ from mcp_server_python_docs.retrieval.ranker import (
|
|
|
23
25
|
from mcp_server_python_docs.services.observability import log_tool_call
|
|
24
26
|
from mcp_server_python_docs.services.version_resolution import resolve_version_permissive
|
|
25
27
|
|
|
28
|
+
_DOTTED_IDENTIFIER = re.compile(r"\b[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)+\b")
|
|
29
|
+
|
|
26
30
|
|
|
27
31
|
class SearchService:
|
|
28
32
|
"""Search service dispatching queries through classifier, synonym expansion,
|
|
@@ -55,6 +59,47 @@ class SearchService:
|
|
|
55
59
|
).fetchone()
|
|
56
60
|
return row is not None
|
|
57
61
|
|
|
62
|
+
def _known_identifiers(self, query: str, version: str) -> set[str]:
|
|
63
|
+
"""Count exact prompt identifiers only in the requested inventory."""
|
|
64
|
+
return {
|
|
65
|
+
name
|
|
66
|
+
for name in set(_DOTTED_IDENTIFIER.findall(query))
|
|
67
|
+
if self._db.execute(
|
|
68
|
+
"SELECT 1 FROM symbols s JOIN doc_sets d ON d.id = s.doc_set_id "
|
|
69
|
+
"WHERE s.qualified_name = ? AND d.version = ? LIMIT 1",
|
|
70
|
+
(name, version),
|
|
71
|
+
).fetchone() is not None
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
def _append_canonical(
|
|
75
|
+
self, original: SearchDocsResult, query: str, version: str | None,
|
|
76
|
+
kind: str, max_results: int,
|
|
77
|
+
) -> SearchDocsResult:
|
|
78
|
+
"""Fill at most one spare slot without altering semantic evidence."""
|
|
79
|
+
if kind != "auto" or version is None or not original.hits:
|
|
80
|
+
return original
|
|
81
|
+
if len(original.hits) >= max_results:
|
|
82
|
+
return original
|
|
83
|
+
known = self._known_identifiers(query, version)
|
|
84
|
+
if len(known) != 1:
|
|
85
|
+
return original
|
|
86
|
+
name = known.pop()
|
|
87
|
+
candidates = lookup_symbols_exact(self._db, name, version, 1)
|
|
88
|
+
if not candidates or candidates[0].title != name:
|
|
89
|
+
return original
|
|
90
|
+
candidate = candidates[0]
|
|
91
|
+
|
|
92
|
+
def location(hit: SymbolHit) -> tuple[str, str, str]:
|
|
93
|
+
uri = urlsplit(hit.uri)
|
|
94
|
+
return (hit.version, (hit.slug or uri.path).removesuffix(".html"),
|
|
95
|
+
hit.anchor or uri.fragment)
|
|
96
|
+
|
|
97
|
+
if any(location(hit) == location(candidate) for hit in original.hits):
|
|
98
|
+
return original
|
|
99
|
+
proposed = original.model_copy(update={"hits": [*original.hits, candidate]})
|
|
100
|
+
# Admission only: never cap, replace or truncate the original response.
|
|
101
|
+
return proposed if len(proposed.model_dump_json().encode("utf-8")) <= 8000 else original
|
|
102
|
+
|
|
58
103
|
@log_tool_call("search_docs")
|
|
59
104
|
def search(
|
|
60
105
|
self,
|
|
@@ -84,7 +129,9 @@ class SearchService:
|
|
|
84
129
|
hits = lookup_symbols_exact(self._db, query, resolved_version, max_results)
|
|
85
130
|
if hits:
|
|
86
131
|
self._last_resolution = "exact"
|
|
87
|
-
return
|
|
132
|
+
return self._append_canonical(
|
|
133
|
+
SearchDocsResult(hits=hits), query, resolved_version, kind, max_results,
|
|
134
|
+
)
|
|
88
135
|
# Fall through to FTS if symbol lookup found nothing and kind is auto
|
|
89
136
|
if kind == "symbol":
|
|
90
137
|
self._last_resolution = "exact"
|
|
@@ -108,4 +155,18 @@ class SearchService:
|
|
|
108
155
|
hits = search_symbols(self._db, match_expr, resolved_version, max_results)
|
|
109
156
|
|
|
110
157
|
self._last_resolution = "fts"
|
|
111
|
-
|
|
158
|
+
if not hits and kind == "auto" and resolved_version is not None:
|
|
159
|
+
# Prompt-only candidates; ambiguity is decided in the requested inventory.
|
|
160
|
+
known = self._known_identifiers(query, resolved_version)
|
|
161
|
+
if len(known) == 1:
|
|
162
|
+
name = known.pop()
|
|
163
|
+
hits = lookup_symbols_exact(self._db, name, resolved_version, max_results)
|
|
164
|
+
if hits:
|
|
165
|
+
self._last_resolution = "exact"
|
|
166
|
+
return SearchDocsResult(
|
|
167
|
+
hits=hits,
|
|
168
|
+
note=f"No full-query results; showing symbol matches for {name!r}.",
|
|
169
|
+
)
|
|
170
|
+
return self._append_canonical(
|
|
171
|
+
SearchDocsResult(hits=hits), query, resolved_version, kind, max_results,
|
|
172
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: python-docs-mcp-server
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
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
|
|
@@ -17,7 +17,7 @@ Requires-Python: >=3.12
|
|
|
17
17
|
Requires-Dist: beautifulsoup4<5.0,>=4.12
|
|
18
18
|
Requires-Dist: click<9.0,>=8.1.7
|
|
19
19
|
Requires-Dist: markdownify<2.0,>=0.14
|
|
20
|
-
Requires-Dist: mcp<2.0.0,>=1.
|
|
20
|
+
Requires-Dist: mcp<2.0.0,>=1.30.0
|
|
21
21
|
Requires-Dist: platformdirs>=4.0
|
|
22
22
|
Requires-Dist: pydantic<3.0,>=2.0.0
|
|
23
23
|
Requires-Dist: pyyaml<7.0,>=6.0
|
|
@@ -48,6 +48,13 @@ Built for the moment your agent needs `asyncio.TaskGroup` signatures, `pathlib.P
|
|
|
48
48
|
|
|
49
49
|
## Why this exists
|
|
50
50
|
|
|
51
|
+
There is a difference between long context and usable context. Research like the
|
|
52
|
+
[*Lost in the Middle*](https://arxiv.org/abs/2307.03172) study (Liu et al., 2023)
|
|
53
|
+
found that models struggle to use relevant information buried in the middle of a
|
|
54
|
+
long input. Pasting an entire documentation page into the model invites that
|
|
55
|
+
failure. Returning the exact symbol, section, and version keeps the answer in a
|
|
56
|
+
short, high-signal context instead.
|
|
57
|
+
|
|
51
58
|
Generic docs retrieval is a rough fit for Python stdlib questions:
|
|
52
59
|
|
|
53
60
|
- `asyncio.TaskGroup` should resolve to the actual symbol, not a fuzzy page hit
|
|
@@ -120,6 +127,20 @@ pipx install python-docs-mcp-server
|
|
|
120
127
|
If `uv` is installed but the `uv` command is not on your `PATH`, reopen your
|
|
121
128
|
shell or use `python -m uv ...` as a fallback for local contributor commands.
|
|
122
129
|
|
|
130
|
+
## Search result behavior
|
|
131
|
+
|
|
132
|
+
For `kind="auto"` with an explicit version, a nonempty search with spare result
|
|
133
|
+
slots may append **one** exact inventory target when the prompt contains one
|
|
134
|
+
known qualified identifier in that version. Original hits, snippets, scores,
|
|
135
|
+
order and notes are preserved. Full results, ambiguous identifiers, other kinds
|
|
136
|
+
and versionless queries are unchanged; the existing empty-result fallback is
|
|
137
|
+
unchanged. Canonical locations are deduplicated, including inventory aliases.
|
|
138
|
+
|
|
139
|
+
The append is admitted only when the **complete proposed response** serialized
|
|
140
|
+
by Pydantic's compact `model_dump_json()` is at most **8000 UTF-8 bytes**. This is
|
|
141
|
+
not a global search cap: oversized original responses remain unchanged. It is
|
|
142
|
+
separate from `get_docs(max_chars=8000)`, which limits content **characters**.
|
|
143
|
+
|
|
123
144
|
## First run
|
|
124
145
|
|
|
125
146
|
Build the local documentation index:
|
|
@@ -140,6 +161,26 @@ cache. It can take several minutes.
|
|
|
140
161
|
|
|
141
162
|
## Configure your MCP client
|
|
142
163
|
|
|
164
|
+
### Claude Code
|
|
165
|
+
|
|
166
|
+
This repository includes a project-scoped `.mcp.json` for clients that support
|
|
167
|
+
checked-in MCP server configuration. It points at the published package:
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"mcpServers": {
|
|
172
|
+
"python-docs": {
|
|
173
|
+
"type": "stdio",
|
|
174
|
+
"command": "uvx",
|
|
175
|
+
"args": ["python-docs-mcp-server"]
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Build the local documentation index with the first-run command above before
|
|
182
|
+
expecting docs queries to return corpus-backed results.
|
|
183
|
+
|
|
143
184
|
### Claude Desktop
|
|
144
185
|
|
|
145
186
|
Add this to your Claude Desktop configuration file:
|
|
@@ -217,6 +258,16 @@ The server currently exposes six MCP tools:
|
|
|
217
258
|
| `detect_python_version` | Detect the user's local Python version and report whether that version has been indexed. |
|
|
218
259
|
| `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. |
|
|
219
260
|
|
|
261
|
+
For `search_docs` with `kind="auto"` and an explicit indexed `version`, the
|
|
262
|
+
original lookup and full-text search run first. If both return no hits, the
|
|
263
|
+
server looks for dotted Python identifiers in the prompt. Exactly one distinct
|
|
264
|
+
identifier registered in that version enables a symbol-lookup fallback; repeated
|
|
265
|
+
mentions count once and unknown identifiers do not count. Multiple known
|
|
266
|
+
identifiers leave the result empty. A result note identifies fallback matches,
|
|
267
|
+
which are symbol documentation, not an answer to the complete prompt. Explicit
|
|
268
|
+
kinds, searches without a version, nonempty results, and result budgets are
|
|
269
|
+
unchanged.
|
|
270
|
+
|
|
220
271
|
## Why not Context7 or generic docs retrieval?
|
|
221
272
|
|
|
222
273
|
Use this server when you want precise local Python docs retrieval rather than
|
|
@@ -14,26 +14,26 @@ mcp_server_python_docs/ingestion/__init__.py,sha256=Yd7Jk7hD7XG3eoBVQpYiG94EwJGf
|
|
|
14
14
|
mcp_server_python_docs/ingestion/cpython_versions.py,sha256=_28TX26eGEiiEV_6NthaPg8vwvwjuus2yVLn7ktvcyo,1426
|
|
15
15
|
mcp_server_python_docs/ingestion/inventory.py,sha256=5gQCWOo1tBiQgv-e_A9KR9n2xfP4YAnJrSmb0pFbzwY,5672
|
|
16
16
|
mcp_server_python_docs/ingestion/publish.py,sha256=gra87NgH1i4T92nSwf3sqp4AnKuuzQfrcE7ySnDgcSQ,17247
|
|
17
|
-
mcp_server_python_docs/ingestion/sphinx_json.py,sha256=
|
|
17
|
+
mcp_server_python_docs/ingestion/sphinx_json.py,sha256=ZSiTK30-9hhra7oFelUL8OP3eB_dB8ddmFBV4hWUL3g,21524
|
|
18
18
|
mcp_server_python_docs/retrieval/__init__.py,sha256=xSH2dWrIun_Q980ElcoXWBIobTqz0am66-AkEvRo_ck,779
|
|
19
19
|
mcp_server_python_docs/retrieval/budget.py,sha256=kORd1Pt6IxLH95vRE7WBwdwezxvk98deYBgeNLEmyOw,2098
|
|
20
20
|
mcp_server_python_docs/retrieval/query.py,sha256=9rWBvWQG3KwVjCqEqCYKMFgKqswU-O4a1SyJiTzQrjk,6116
|
|
21
|
-
mcp_server_python_docs/retrieval/ranker.py,sha256=
|
|
21
|
+
mcp_server_python_docs/retrieval/ranker.py,sha256=ct9uIir0oK38v0eI-lKPq9m_EsN8Fsf3NBajbYi2fAs,14122
|
|
22
22
|
mcp_server_python_docs/services/__init__.py,sha256=UKlaRQYo_cnLOUVf_QieXnHlcnH9ejEa3pYgHhJiG1E,320
|
|
23
23
|
mcp_server_python_docs/services/cache.py,sha256=ADqg89johOJRYZysbes1bEs6nLFrmzS9L5c-Njt1mrU,3199
|
|
24
24
|
mcp_server_python_docs/services/compare.py,sha256=wWsgD8w9fl3L7gr-mcdd6ntY_V1pGtZ-ues1iyyNRPg,12743
|
|
25
|
-
mcp_server_python_docs/services/content.py,sha256=
|
|
25
|
+
mcp_server_python_docs/services/content.py,sha256=5_FPgyOYbax7qNH6OXEePnyfmyjBdgh-QkK7CvEMCCU,4654
|
|
26
26
|
mcp_server_python_docs/services/observability.py,sha256=rwnkIDelNkclHzuwsktGUoU2fzYCuiR1niVRmOwJHJg,4839
|
|
27
27
|
mcp_server_python_docs/services/package_docs.py,sha256=0Qq8PtRL_KdiTFUtllaj4KFHz6u9L903zcORVpYmR1U,5605
|
|
28
28
|
mcp_server_python_docs/services/persistent_cache.py,sha256=p1uhTkwmXRP-v79jaIPDTEv9Z0G0iqCUnUDKmFQA6GA,6956
|
|
29
|
-
mcp_server_python_docs/services/search.py,sha256=
|
|
29
|
+
mcp_server_python_docs/services/search.py,sha256=xioiLrgTPTU9LHg2t0dG8mHxaKwYSKI-haGZq6J0kYs,7384
|
|
30
30
|
mcp_server_python_docs/services/version.py,sha256=nPkWhksZo7B4QIp-knlSEvVXFmKIcVUq6tWRTJAe5aA,1467
|
|
31
31
|
mcp_server_python_docs/services/version_resolution.py,sha256=rULBoqq97t0ZfSWSy-ecI3wALx5t_3OWuok1hZK8w0w,2643
|
|
32
32
|
mcp_server_python_docs/storage/__init__.py,sha256=YNeNr7FW9Vgvo79oXgKXE3eGP0WwWDqRgTZ7lg7MFW4,62
|
|
33
33
|
mcp_server_python_docs/storage/db.py,sha256=Lys8HN3IvgmNpr_qyocUQghcZnwHf2lz9eq_BFmwi54,5634
|
|
34
34
|
mcp_server_python_docs/storage/schema.sql,sha256=4cXPktPQSLfbjkSjirpf0dA2CphlO9UCIKUxnLiIm_g,5637
|
|
35
|
-
python_docs_mcp_server-0.3.
|
|
36
|
-
python_docs_mcp_server-0.3.
|
|
37
|
-
python_docs_mcp_server-0.3.
|
|
38
|
-
python_docs_mcp_server-0.3.
|
|
39
|
-
python_docs_mcp_server-0.3.
|
|
35
|
+
python_docs_mcp_server-0.3.2.dist-info/METADATA,sha256=-qJFihR4PRVHt3lqrIbY8asRrD1XQt94RCRcUbQ5CgA,16256
|
|
36
|
+
python_docs_mcp_server-0.3.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
37
|
+
python_docs_mcp_server-0.3.2.dist-info/entry_points.txt,sha256=-1arbtjZhF5mtnJNAh_90UBr1f4AqJxT3Tmfy9p7NMI,142
|
|
38
|
+
python_docs_mcp_server-0.3.2.dist-info/licenses/LICENSE,sha256=-AHGWRwcvoeP4bkY1W2ok_qaI1Mg0Zx2qYgR1k-3CHY,1071
|
|
39
|
+
python_docs_mcp_server-0.3.2.dist-info/RECORD,,
|
{python_docs_mcp_server-0.3.0.dist-info → python_docs_mcp_server-0.3.2.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{python_docs_mcp_server-0.3.0.dist-info → python_docs_mcp_server-0.3.2.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|