python-docs-mcp-server 0.2.0__tar.gz → 0.2.1__tar.gz
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.
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/CHANGELOG.md +12 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/PKG-INFO +1 -1
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/pyproject.toml +1 -1
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/server.json +2 -2
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/ingestion/sphinx_json.py +21 -1
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_ingestion.py +13 -1
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_mcp_get_docs_cache_smoke.py +5 -8
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_stdio_smoke.py +82 -68
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/uv.lock +1 -1
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.cursorignore +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.github/INTEGRATION-TEST.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.github/RELEASE.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.github/TEST-STRATEGY.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.github/dependabot.yml +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.github/workflows/ci.yml +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.github/workflows/codeql.yml +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.github/workflows/e2e.yml +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.github/workflows/release.yml +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.github/workflows/scorecard.yml +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.github/workflows/security.yml +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.gitignore +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.planning/POSITIONING.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.planning/ROADMAP.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.planning/phases/09-compare-versions/09-01-data-shape-spike-PLAN.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.planning/phases/09-compare-versions/09-01-data-shape-spike-SUMMARY.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.planning/phases/09-compare-versions/09-02-result-models-PLAN.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.planning/phases/09-compare-versions/09-02-result-models-SUMMARY.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.planning/phases/09-compare-versions/09-03-compare-service-PLAN.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.planning/phases/09-compare-versions/09-03-compare-service-SUMMARY.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.planning/phases/09-compare-versions/09-04-mcp-tool-wiring-PLAN.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.planning/phases/09-compare-versions/09-04-mcp-tool-wiring-SUMMARY.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.planning/phases/09-compare-versions/09-05-user-facing-docs-PLAN.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.planning/phases/09-compare-versions/09-05-user-facing-docs-SUMMARY.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.planning/phases/09-compare-versions/09-CONTEXT.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.planning/phases/09-compare-versions/09-REVIEW.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.planning/phases/09-compare-versions/09-VERIFICATION.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.planning/phases/10-whatsnew/10-CONTEXT.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.planning/phases/11-detect-venv/11-CONTEXT.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/AGENTS.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/CLAUDE.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/CONTRIBUTING.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/LICENSE +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/README.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/SECURITY.md +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/glama.json +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/__init__.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/__main__.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/app_context.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/data/synonyms.yaml +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/detection.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/diagnostics.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/errors.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/ingestion/__init__.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/ingestion/cpython_versions.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/ingestion/inventory.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/ingestion/publish.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/models.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/py.typed +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/retrieval/__init__.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/retrieval/budget.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/retrieval/query.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/retrieval/ranker.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/server.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/services/__init__.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/services/cache.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/services/compare.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/services/content.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/services/observability.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/services/package_docs.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/services/persistent_cache.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/services/search.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/services/version.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/services/version_resolution.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/storage/__init__.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/storage/db.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/storage/schema.sql +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/__init__.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/conftest.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/fixtures/retrieval_regression_cases.json +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/fixtures/sample_broken.fjson +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/fixtures/sample_library.fjson +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/fixtures/schema-get_docs-input.json +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/fixtures/schema-get_docs-output.json +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/fixtures/schema-list_versions-output.json +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/fixtures/schema-search_docs-input.json +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/fixtures/schema-search_docs-output.json +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_ci_workflows.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_compare_versions.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_compare_versions_spike.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_detection.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_doctor.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_multi_version.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_package_docs.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_packaging.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_persistent_docs_cache.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_phase1_integration.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_publish.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_retrieval.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_retrieval_regression.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_schema.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_schema_snapshot.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_services.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_stability.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_stdio_hygiene.py +0 -0
- {python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_synonyms.py +0 -0
|
@@ -4,6 +4,18 @@ All notable changes to `python-docs-mcp-server` are documented here.
|
|
|
4
4
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
|
|
5
5
|
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.2.1] — 2026-05-29
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`build-index` on Python 3.14** — Sphinx's parallel JSON build (`-j auto`)
|
|
12
|
+
raised `_pickle.PicklingError` on Python 3.14, which flipped the default
|
|
13
|
+
`multiprocessing` start method on POSIX from `fork` to `forkserver`
|
|
14
|
+
(`forkserver` must pickle worker tasks, but Sphinx's `ParallelTasks` holds an
|
|
15
|
+
unpicklable local closure). The builder now falls back to a serial build
|
|
16
|
+
(`-j 1`) on 3.14+ while keeping the parallel path on `fork`-default
|
|
17
|
+
interpreters (3.13 and earlier). Server runtime behavior is unchanged.
|
|
18
|
+
|
|
7
19
|
## [0.2.0] — 2026-05-29
|
|
8
20
|
|
|
9
21
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-docs-mcp-server
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-docs-mcp-server"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "The canonical Python stdlib oracle for AI coding agents — exact symbols, exact sections, exact versions, offline, always free, always MIT, token-frugal."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
"url": "https://github.com/ayhammouda/python-docs-mcp-server",
|
|
9
9
|
"source": "github"
|
|
10
10
|
},
|
|
11
|
-
"version": "0.2.
|
|
11
|
+
"version": "0.2.1",
|
|
12
12
|
"packages": [
|
|
13
13
|
{
|
|
14
14
|
"registryType": "pypi",
|
|
15
15
|
"registryBaseUrl": "https://pypi.org",
|
|
16
16
|
"identifier": "python-docs-mcp-server",
|
|
17
|
-
"version": "0.2.
|
|
17
|
+
"version": "0.2.1",
|
|
18
18
|
"runtimeHint": "uvx",
|
|
19
19
|
"transport": {
|
|
20
20
|
"type": "stdio"
|
|
@@ -13,6 +13,7 @@ import logging
|
|
|
13
13
|
import os
|
|
14
14
|
import re
|
|
15
15
|
import sqlite3
|
|
16
|
+
import sys
|
|
16
17
|
from collections.abc import Mapping
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
|
|
@@ -166,6 +167,25 @@ def make_sphinx_json_env(
|
|
|
166
167
|
return env
|
|
167
168
|
|
|
168
169
|
|
|
170
|
+
def sphinx_parallel_jobs(
|
|
171
|
+
version_info: tuple[int, ...] = (sys.version_info.major, sys.version_info.minor),
|
|
172
|
+
) -> str:
|
|
173
|
+
"""Return the Sphinx ``-j`` value for the build interpreter.
|
|
174
|
+
|
|
175
|
+
The Sphinx venv is created from the current interpreter (``venv.create``),
|
|
176
|
+
so ``sys.version_info`` reflects the Python that will run ``sphinx-build``.
|
|
177
|
+
|
|
178
|
+
Python 3.14 changed the default ``multiprocessing`` start method on
|
|
179
|
+
POSIX from ``fork`` to ``forkserver``. ``forkserver`` pickles the work
|
|
180
|
+
handed to child processes, but Sphinx's parallel build passes a
|
|
181
|
+
``ParallelTasks`` object holding a local closure
|
|
182
|
+
(``Builder._read_parallel.<locals>.merge``) that cannot be pickled, so
|
|
183
|
+
``-j auto`` raises ``_pickle.PicklingError`` on 3.14+. Fall back to a
|
|
184
|
+
serial build there; ``fork``-default interpreters keep the parallel path.
|
|
185
|
+
"""
|
|
186
|
+
return "1" if version_info >= (3, 14) else "auto"
|
|
187
|
+
|
|
188
|
+
|
|
169
189
|
def build_sphinx_json_command(
|
|
170
190
|
sphinx_build: Path | str,
|
|
171
191
|
doc_dir: Path | str,
|
|
@@ -179,7 +199,7 @@ def build_sphinx_json_command(
|
|
|
179
199
|
"-D",
|
|
180
200
|
"html_theme=classic",
|
|
181
201
|
"-j",
|
|
182
|
-
|
|
202
|
+
sphinx_parallel_jobs(),
|
|
183
203
|
str(doc_dir),
|
|
184
204
|
str(json_out),
|
|
185
205
|
]
|
|
@@ -34,6 +34,7 @@ from mcp_server_python_docs.ingestion.sphinx_json import (
|
|
|
34
34
|
parse_fjson,
|
|
35
35
|
populate_synonyms,
|
|
36
36
|
rebuild_fts_indexes,
|
|
37
|
+
sphinx_parallel_jobs,
|
|
37
38
|
write_json_build_requirements,
|
|
38
39
|
write_sphinx_json_sitecustomize,
|
|
39
40
|
)
|
|
@@ -227,11 +228,22 @@ class TestSphinxJsonCommand:
|
|
|
227
228
|
"-D",
|
|
228
229
|
"html_theme=classic",
|
|
229
230
|
"-j",
|
|
230
|
-
|
|
231
|
+
sphinx_parallel_jobs(),
|
|
231
232
|
str(doc_dir),
|
|
232
233
|
str(json_out),
|
|
233
234
|
]
|
|
234
235
|
|
|
236
|
+
def test_sphinx_parallel_jobs_auto_before_314(self):
|
|
237
|
+
"""Pre-3.14 interpreters keep the parallel 'auto' build (fork default)."""
|
|
238
|
+
assert sphinx_parallel_jobs((3, 10, 0)) == "auto"
|
|
239
|
+
assert sphinx_parallel_jobs((3, 13, 5)) == "auto"
|
|
240
|
+
|
|
241
|
+
def test_sphinx_parallel_jobs_serial_on_314_plus(self):
|
|
242
|
+
"""3.14+ flipped multiprocessing to forkserver, which can't pickle
|
|
243
|
+
Sphinx's parallel ParallelTasks closures -> force a serial build."""
|
|
244
|
+
assert sphinx_parallel_jobs((3, 14, 0)) == "1"
|
|
245
|
+
assert sphinx_parallel_jobs((3, 15, 1)) == "1"
|
|
246
|
+
|
|
235
247
|
|
|
236
248
|
# ── fjson parsing tests (INGR-C-04) ──
|
|
237
249
|
|
{python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_mcp_get_docs_cache_smoke.py
RENAMED
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import sqlite3
|
|
5
5
|
import subprocess
|
|
6
|
-
import sys
|
|
7
6
|
from pathlib import Path
|
|
8
7
|
|
|
9
8
|
from mcp_server_python_docs.services.persistent_cache import _NO_ANCHOR_KEY
|
|
@@ -14,6 +13,7 @@ from tests.test_stdio_smoke import (
|
|
|
14
13
|
_isolated_cache_env,
|
|
15
14
|
_make_notification,
|
|
16
15
|
_make_request,
|
|
16
|
+
_run_server_until_responses,
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
|
|
@@ -80,13 +80,10 @@ def _create_contentful_json_index(cache_dir: Path) -> Path:
|
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
def _run_server(stdin_data: bytes, env: dict[str, str]) -> subprocess.CompletedProcess:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
timeout=15,
|
|
88
|
-
env=env,
|
|
89
|
-
)
|
|
83
|
+
# Use the polling runner (waits for the tools/call reply before closing
|
|
84
|
+
# stdin) instead of subprocess.run(input=...), which races the server's
|
|
85
|
+
# EOF-driven shutdown and flakes on cold CI runners.
|
|
86
|
+
return _run_server_until_responses(stdin_data, env)
|
|
90
87
|
|
|
91
88
|
|
|
92
89
|
def _initialized_tool_call(name: str, arguments: dict, req_id: int = 2) -> bytes:
|
|
@@ -184,6 +184,87 @@ def _request_ids(stdin_data: bytes) -> list[int]:
|
|
|
184
184
|
return request_ids
|
|
185
185
|
|
|
186
186
|
|
|
187
|
+
def _run_server_until_responses(
|
|
188
|
+
stdin_data: bytes, env: dict[str, str], timeout: int = 15,
|
|
189
|
+
) -> subprocess.CompletedProcess:
|
|
190
|
+
"""Run the MCP server, stream stdin, and wait for every expected response.
|
|
191
|
+
|
|
192
|
+
Unlike a fire-and-forget ``subprocess.run(input=...)``, this keeps stdin
|
|
193
|
+
open until each request id in ``stdin_data`` has a matching response on
|
|
194
|
+
stdout (or the timeout/exit fires). That avoids a race where the server's
|
|
195
|
+
read loop shuts down on stdin EOF before the final ``tools/call`` handler
|
|
196
|
+
flushes its reply — the source of CI flakiness on cold runners.
|
|
197
|
+
"""
|
|
198
|
+
proc = subprocess.Popen(
|
|
199
|
+
[sys.executable, "-m", "mcp_server_python_docs", "serve"],
|
|
200
|
+
stdin=subprocess.PIPE,
|
|
201
|
+
stdout=subprocess.PIPE,
|
|
202
|
+
stderr=subprocess.PIPE,
|
|
203
|
+
env=env,
|
|
204
|
+
)
|
|
205
|
+
assert proc.stdin is not None
|
|
206
|
+
assert proc.stdout is not None
|
|
207
|
+
assert proc.stderr is not None
|
|
208
|
+
|
|
209
|
+
stdout_lines: list[bytes] = []
|
|
210
|
+
stderr_lines: list[bytes] = []
|
|
211
|
+
output_lock = threading.Lock()
|
|
212
|
+
|
|
213
|
+
def read_stream(stream, sink: list[bytes]) -> None:
|
|
214
|
+
for line in iter(stream.readline, b""):
|
|
215
|
+
with output_lock:
|
|
216
|
+
sink.append(line)
|
|
217
|
+
|
|
218
|
+
stdout_thread = threading.Thread(
|
|
219
|
+
target=read_stream,
|
|
220
|
+
args=(proc.stdout, stdout_lines),
|
|
221
|
+
daemon=True,
|
|
222
|
+
)
|
|
223
|
+
stderr_thread = threading.Thread(
|
|
224
|
+
target=read_stream,
|
|
225
|
+
args=(proc.stderr, stderr_lines),
|
|
226
|
+
daemon=True,
|
|
227
|
+
)
|
|
228
|
+
stdout_thread.start()
|
|
229
|
+
stderr_thread.start()
|
|
230
|
+
|
|
231
|
+
expected_ids = _request_ids(stdin_data)
|
|
232
|
+
deadline = time.monotonic() + timeout
|
|
233
|
+
try:
|
|
234
|
+
for line in stdin_data.splitlines(keepends=True):
|
|
235
|
+
proc.stdin.write(line)
|
|
236
|
+
proc.stdin.flush()
|
|
237
|
+
|
|
238
|
+
while time.monotonic() < deadline:
|
|
239
|
+
with output_lock:
|
|
240
|
+
responses = _read_responses(b"".join(stdout_lines))
|
|
241
|
+
if all(_find_response(responses, req_id) is not None for req_id in expected_ids):
|
|
242
|
+
break
|
|
243
|
+
if proc.poll() is not None:
|
|
244
|
+
break
|
|
245
|
+
time.sleep(0.02)
|
|
246
|
+
finally:
|
|
247
|
+
try:
|
|
248
|
+
proc.stdin.close()
|
|
249
|
+
except BrokenPipeError:
|
|
250
|
+
# The subprocess may have already closed stdin after handling the requests.
|
|
251
|
+
pass
|
|
252
|
+
proc.stdin = None
|
|
253
|
+
|
|
254
|
+
remaining = max(0.1, deadline - time.monotonic())
|
|
255
|
+
try:
|
|
256
|
+
proc.wait(timeout=remaining)
|
|
257
|
+
except subprocess.TimeoutExpired:
|
|
258
|
+
proc.kill()
|
|
259
|
+
proc.wait(timeout=5)
|
|
260
|
+
|
|
261
|
+
stdout_thread.join(timeout=1)
|
|
262
|
+
stderr_thread.join(timeout=1)
|
|
263
|
+
stdout = b"".join(stdout_lines)
|
|
264
|
+
stderr = b"".join(stderr_lines)
|
|
265
|
+
return subprocess.CompletedProcess(proc.args, proc.returncode, stdout, stderr)
|
|
266
|
+
|
|
267
|
+
|
|
187
268
|
class TestStdioSmoke:
|
|
188
269
|
"""Spawn the MCP server as a subprocess and verify protocol compliance."""
|
|
189
270
|
|
|
@@ -198,74 +279,7 @@ class TestStdioSmoke:
|
|
|
198
279
|
self, stdin_data: bytes, timeout: int = 15,
|
|
199
280
|
) -> subprocess.CompletedProcess:
|
|
200
281
|
"""Run the server subprocess with line-paced stdin and return the result."""
|
|
201
|
-
|
|
202
|
-
[sys.executable, "-m", "mcp_server_python_docs", "serve"],
|
|
203
|
-
stdin=subprocess.PIPE,
|
|
204
|
-
stdout=subprocess.PIPE,
|
|
205
|
-
stderr=subprocess.PIPE,
|
|
206
|
-
env=self.env,
|
|
207
|
-
)
|
|
208
|
-
assert proc.stdin is not None
|
|
209
|
-
assert proc.stdout is not None
|
|
210
|
-
assert proc.stderr is not None
|
|
211
|
-
|
|
212
|
-
stdout_lines: list[bytes] = []
|
|
213
|
-
stderr_lines: list[bytes] = []
|
|
214
|
-
output_lock = threading.Lock()
|
|
215
|
-
|
|
216
|
-
def read_stream(stream, sink: list[bytes]) -> None:
|
|
217
|
-
for line in iter(stream.readline, b""):
|
|
218
|
-
with output_lock:
|
|
219
|
-
sink.append(line)
|
|
220
|
-
|
|
221
|
-
stdout_thread = threading.Thread(
|
|
222
|
-
target=read_stream,
|
|
223
|
-
args=(proc.stdout, stdout_lines),
|
|
224
|
-
daemon=True,
|
|
225
|
-
)
|
|
226
|
-
stderr_thread = threading.Thread(
|
|
227
|
-
target=read_stream,
|
|
228
|
-
args=(proc.stderr, stderr_lines),
|
|
229
|
-
daemon=True,
|
|
230
|
-
)
|
|
231
|
-
stdout_thread.start()
|
|
232
|
-
stderr_thread.start()
|
|
233
|
-
|
|
234
|
-
expected_ids = _request_ids(stdin_data)
|
|
235
|
-
deadline = time.monotonic() + timeout
|
|
236
|
-
try:
|
|
237
|
-
for line in stdin_data.splitlines(keepends=True):
|
|
238
|
-
proc.stdin.write(line)
|
|
239
|
-
proc.stdin.flush()
|
|
240
|
-
|
|
241
|
-
while time.monotonic() < deadline:
|
|
242
|
-
with output_lock:
|
|
243
|
-
responses = _read_responses(b"".join(stdout_lines))
|
|
244
|
-
if all(_find_response(responses, req_id) is not None for req_id in expected_ids):
|
|
245
|
-
break
|
|
246
|
-
if proc.poll() is not None:
|
|
247
|
-
break
|
|
248
|
-
time.sleep(0.02)
|
|
249
|
-
finally:
|
|
250
|
-
try:
|
|
251
|
-
proc.stdin.close()
|
|
252
|
-
except BrokenPipeError:
|
|
253
|
-
# The subprocess may have already closed stdin after handling the requests.
|
|
254
|
-
pass
|
|
255
|
-
proc.stdin = None
|
|
256
|
-
|
|
257
|
-
remaining = max(0.1, deadline - time.monotonic())
|
|
258
|
-
try:
|
|
259
|
-
proc.wait(timeout=remaining)
|
|
260
|
-
except subprocess.TimeoutExpired:
|
|
261
|
-
proc.kill()
|
|
262
|
-
proc.wait(timeout=5)
|
|
263
|
-
|
|
264
|
-
stdout_thread.join(timeout=1)
|
|
265
|
-
stderr_thread.join(timeout=1)
|
|
266
|
-
stdout = b"".join(stdout_lines)
|
|
267
|
-
stderr = b"".join(stderr_lines)
|
|
268
|
-
return subprocess.CompletedProcess(proc.args, proc.returncode, stdout, stderr)
|
|
282
|
+
return _run_server_until_responses(stdin_data, self.env, timeout)
|
|
269
283
|
|
|
270
284
|
def test_server_lists_tools_no_stdout_pollution(self):
|
|
271
285
|
"""Server returns tool list and stdout has no non-JSON-RPC bytes."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.github/workflows/scorecard.yml
RENAMED
|
File without changes
|
{python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/.github/workflows/security.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/__init__.py
RENAMED
|
File without changes
|
{python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/errors.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/models.py
RENAMED
|
File without changes
|
{python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/src/mcp_server_python_docs/server.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/fixtures/sample_broken.fjson
RENAMED
|
File without changes
|
{python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/fixtures/sample_library.fjson
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_compare_versions.py
RENAMED
|
File without changes
|
{python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_compare_versions_spike.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_persistent_docs_cache.py
RENAMED
|
File without changes
|
{python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_phase1_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_docs_mcp_server-0.2.0 → python_docs_mcp_server-0.2.1}/tests/test_retrieval_regression.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|