exactdocs 0.1.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.
Files changed (49) hide show
  1. exactdocs/__init__.py +5 -0
  2. exactdocs/__main__.py +3 -0
  3. exactdocs/_version.py +6 -0
  4. exactdocs/_web/assets/index-B350HwjP.js +10 -0
  5. exactdocs/_web/assets/index-CG8rwfz9.css +2 -0
  6. exactdocs/_web/assets/routes-CCGZxbYl.js +1 -0
  7. exactdocs/_web/index.html +13 -0
  8. exactdocs/bootstrap/__init__.py +5 -0
  9. exactdocs/bootstrap/bridges.py +27 -0
  10. exactdocs/bootstrap/cli.py +48 -0
  11. exactdocs/bootstrap/container.py +28 -0
  12. exactdocs/bootstrap/http.py +90 -0
  13. exactdocs/bootstrap/mcp.py +21 -0
  14. exactdocs/bootstrap/settings.py +15 -0
  15. exactdocs/catalog/__init__.py +9 -0
  16. exactdocs/catalog/adapters/__init__.py +0 -0
  17. exactdocs/catalog/adapters/driven/__init__.py +0 -0
  18. exactdocs/catalog/adapters/driven/memory.py +35 -0
  19. exactdocs/catalog/adapters/driving/__init__.py +0 -0
  20. exactdocs/catalog/adapters/driving/http.py +21 -0
  21. exactdocs/catalog/adapters/driving/mcp.py +18 -0
  22. exactdocs/catalog/dtos.py +42 -0
  23. exactdocs/catalog/errors.py +16 -0
  24. exactdocs/catalog/models.py +34 -0
  25. exactdocs/catalog/module.py +27 -0
  26. exactdocs/catalog/ports/__init__.py +4 -0
  27. exactdocs/catalog/ports/driven.py +18 -0
  28. exactdocs/catalog/ports/driving.py +21 -0
  29. exactdocs/catalog/use_cases.py +33 -0
  30. exactdocs/py.typed +0 -0
  31. exactdocs/retrieval/__init__.py +8 -0
  32. exactdocs/retrieval/adapters/__init__.py +0 -0
  33. exactdocs/retrieval/adapters/driven/__init__.py +0 -0
  34. exactdocs/retrieval/adapters/driven/memory.py +26 -0
  35. exactdocs/retrieval/adapters/driving/__init__.py +0 -0
  36. exactdocs/retrieval/adapters/driving/http.py +17 -0
  37. exactdocs/retrieval/adapters/driving/mcp.py +28 -0
  38. exactdocs/retrieval/dtos.py +53 -0
  39. exactdocs/retrieval/errors.py +17 -0
  40. exactdocs/retrieval/models.py +34 -0
  41. exactdocs/retrieval/module.py +18 -0
  42. exactdocs/retrieval/ports/__init__.py +4 -0
  43. exactdocs/retrieval/ports/driven.py +26 -0
  44. exactdocs/retrieval/ports/driving.py +13 -0
  45. exactdocs/retrieval/use_cases.py +22 -0
  46. exactdocs-0.1.0.dist-info/METADATA +35 -0
  47. exactdocs-0.1.0.dist-info/RECORD +49 -0
  48. exactdocs-0.1.0.dist-info/WHEEL +4 -0
  49. exactdocs-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,17 @@
1
+ from typing import Annotated
2
+
3
+ from fastapi import APIRouter, Query
4
+
5
+ from exactdocs.retrieval.dtos import SearchDocsQuery, SearchDocsResult
6
+ from exactdocs.retrieval.ports.driving import SearchDocs
7
+
8
+
9
+ def build_router(*, search_docs: SearchDocs) -> APIRouter:
10
+ """Expose retrieval under ``/search``."""
11
+ router = APIRouter(prefix="/search", tags=["retrieval"])
12
+
13
+ @router.get("")
14
+ async def search(query: Annotated[SearchDocsQuery, Query()]) -> SearchDocsResult:
15
+ return await search_docs(query)
16
+
17
+ return router
@@ -0,0 +1,28 @@
1
+ """MCP adapter for retrieval. All tools are read-only."""
2
+
3
+ from typing import Annotated
4
+
5
+ from fastmcp import FastMCP
6
+ from mcp.types import ToolAnnotations
7
+ from pydantic import Field
8
+
9
+ from exactdocs.retrieval.dtos import SearchDocsQuery, SearchDocsResult
10
+ from exactdocs.retrieval.ports.driving import SearchDocs
11
+
12
+ READ_ONLY = ToolAnnotations(read_only_hint=True)
13
+
14
+
15
+ def register_tools(mcp: FastMCP, *, search_docs: SearchDocs) -> None:
16
+ """Register retrieval's MCP tools on ``mcp``."""
17
+
18
+ @mcp.tool(name="search_docs", annotations=READ_ONLY)
19
+ async def search_docs_tool(
20
+ library: Annotated[str, Field(description="Library name, e.g. 'react'")],
21
+ version: Annotated[str, Field(description="Exact indexed version, e.g. '19.1'")],
22
+ query: Annotated[str, Field(description="Identifier, error message or question")],
23
+ limit: Annotated[int, Field(ge=1, le=50)] = 5,
24
+ ) -> SearchDocsResult:
25
+ """Search one library version. Returns verbatim excerpts with source URLs."""
26
+ return await search_docs(
27
+ SearchDocsQuery(library=library, version=version, query=query, limit=limit)
28
+ )
@@ -0,0 +1,53 @@
1
+ """Data crossing the retrieval boundary. Immutable, validated."""
2
+
3
+ from collections.abc import Iterable
4
+ from typing import Self
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field
7
+
8
+ from exactdocs.retrieval.models import Excerpt
9
+
10
+
11
+ class _Dto(BaseModel):
12
+ model_config = ConfigDict(frozen=True, extra="forbid")
13
+
14
+
15
+ class SearchDocsQuery(_Dto):
16
+ """Search within exactly one library version."""
17
+
18
+ library: str = Field(min_length=1)
19
+ version: str = Field(min_length=1)
20
+ query: str = Field(min_length=1)
21
+ limit: int = Field(default=5, ge=1, le=50)
22
+
23
+
24
+ class ExcerptOut(_Dto):
25
+ """One verbatim excerpt with its provenance."""
26
+
27
+ text: str
28
+ source_url: str
29
+ library: str
30
+ version: str
31
+ commit_sha: str | None
32
+
33
+ @classmethod
34
+ def from_model(cls, excerpt: Excerpt) -> Self:
35
+ """Map a domain value object to its boundary representation."""
36
+ return cls(
37
+ text=excerpt.text,
38
+ source_url=excerpt.source_url,
39
+ library=excerpt.target.library,
40
+ version=excerpt.target.version,
41
+ commit_sha=excerpt.commit_sha,
42
+ )
43
+
44
+
45
+ class SearchDocsResult(_Dto):
46
+ """Excerpts ordered from most to least relevant."""
47
+
48
+ excerpts: list[ExcerptOut]
49
+
50
+ @classmethod
51
+ def from_models(cls, excerpts: Iterable[Excerpt]) -> Self:
52
+ """Map domain value objects, keeping their order."""
53
+ return cls(excerpts=[ExcerptOut.from_model(e) for e in excerpts])
@@ -0,0 +1,17 @@
1
+ from exactdocs_kernel import NotFoundError, ValidationError
2
+
3
+
4
+ class EmptySearchTextError(ValidationError):
5
+ """The search text is blank."""
6
+
7
+ def __init__(self) -> None:
8
+ super().__init__("search text must not be empty")
9
+
10
+
11
+ class VersionNotIndexedError(NotFoundError):
12
+ """There is no index for this exact library version, so there is no answer."""
13
+
14
+ def __init__(self, library: str, version: str) -> None:
15
+ super().__init__(f"no index for {library}@{version}")
16
+ self.library = library
17
+ self.version = version
@@ -0,0 +1,34 @@
1
+ """Retrieval value objects. Pure Python: no frameworks, no I/O."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from exactdocs.retrieval.errors import EmptySearchTextError
6
+
7
+
8
+ @dataclass(frozen=True, slots=True, kw_only=True)
9
+ class DocTarget:
10
+ """The exact library version a search is pinned to. Version is a hard filter."""
11
+
12
+ library: str
13
+ version: str
14
+
15
+
16
+ @dataclass(frozen=True, slots=True, kw_only=True)
17
+ class SearchText:
18
+ """What the agent is looking for: an identifier, an error code or a question."""
19
+
20
+ value: str
21
+
22
+ def __post_init__(self) -> None:
23
+ if not self.value.strip():
24
+ raise EmptySearchTextError
25
+
26
+
27
+ @dataclass(frozen=True, slots=True, kw_only=True)
28
+ class Excerpt:
29
+ """Original documentation text, never a summary, with where it came from."""
30
+
31
+ text: str
32
+ source_url: str
33
+ target: DocTarget
34
+ commit_sha: str | None = None
@@ -0,0 +1,18 @@
1
+ from dataclasses import dataclass
2
+ from typing import Self
3
+
4
+ from exactdocs.retrieval.ports.driven import ExcerptIndex, IndexedVersions
5
+ from exactdocs.retrieval.ports.driving import SearchDocs
6
+ from exactdocs.retrieval.use_cases import SearchDocsUseCase
7
+
8
+
9
+ @dataclass(frozen=True, slots=True, kw_only=True)
10
+ class RetrievalModule:
11
+ """Retrieval's driving ports, ready to be exposed by driving adapters."""
12
+
13
+ search_docs: SearchDocs
14
+
15
+ @classmethod
16
+ def build(cls, *, versions: IndexedVersions, index: ExcerptIndex) -> Self:
17
+ """Assemble the use cases around the given driven adapters."""
18
+ return cls(search_docs=SearchDocsUseCase(versions, index))
@@ -0,0 +1,4 @@
1
+ from exactdocs.retrieval.ports.driven import ExcerptIndex, IndexedVersions
2
+ from exactdocs.retrieval.ports.driving import SearchDocs
3
+
4
+ __all__ = ["ExcerptIndex", "IndexedVersions", "SearchDocs"]
@@ -0,0 +1,26 @@
1
+ """Driven (secondary) ports of retrieval."""
2
+
3
+ from collections.abc import Sequence
4
+ from typing import Protocol
5
+
6
+ from exactdocs.retrieval.models import DocTarget, Excerpt, SearchText
7
+
8
+
9
+ class IndexedVersions(Protocol):
10
+ """Which library versions have an index.
11
+
12
+ The catalog domain owns this knowledge. Retrieval states what it needs in its own terms
13
+ and the composition root plugs in an adapter, so neither domain imports the other.
14
+ """
15
+
16
+ async def is_indexed(self, target: DocTarget) -> bool:
17
+ """Return ``True`` only for an exact library and version match."""
18
+ ...
19
+
20
+
21
+ class ExcerptIndex(Protocol):
22
+ """Search backend (BM25 + vectors + rerank) scoped to one library version."""
23
+
24
+ async def search(self, target: DocTarget, text: SearchText, limit: int) -> Sequence[Excerpt]:
25
+ """Return at most ``limit`` excerpts from ``target`` only, best first."""
26
+ ...
@@ -0,0 +1,13 @@
1
+ """Driving (primary) ports of retrieval."""
2
+
3
+ from typing import Protocol
4
+
5
+ from exactdocs.retrieval.dtos import SearchDocsQuery, SearchDocsResult
6
+
7
+
8
+ class SearchDocs(Protocol):
9
+ """Hybrid search within one library version; raises ``VersionNotIndexedError``."""
10
+
11
+ async def __call__(self, query: SearchDocsQuery) -> SearchDocsResult:
12
+ """Run the use case."""
13
+ ...
@@ -0,0 +1,22 @@
1
+ from dataclasses import dataclass
2
+
3
+ from exactdocs.retrieval.dtos import SearchDocsQuery, SearchDocsResult
4
+ from exactdocs.retrieval.errors import VersionNotIndexedError
5
+ from exactdocs.retrieval.models import DocTarget, SearchText
6
+ from exactdocs.retrieval.ports.driven import ExcerptIndex, IndexedVersions
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class SearchDocsUseCase:
11
+ """Implements ``ports.driving.SearchDocs``."""
12
+
13
+ versions: IndexedVersions
14
+ index: ExcerptIndex
15
+
16
+ async def __call__(self, query: SearchDocsQuery) -> SearchDocsResult:
17
+ """Search one exact version; never fall back to another version."""
18
+ target = DocTarget(library=query.library, version=query.version)
19
+ if not await self.versions.is_indexed(target):
20
+ raise VersionNotIndexedError(target.library, target.version)
21
+ excerpts = await self.index.search(target, SearchText(value=query.query), query.limit)
22
+ return SearchDocsResult.from_models(excerpts)
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.5
2
+ Name: exactdocs
3
+ Version: 0.1.0
4
+ Summary: Self-hosted, version-accurate documentation for AI coding agents (MCP server).
5
+ Project-URL: Repository, https://github.com/SirCypkowskyy/ExactDocs
6
+ Project-URL: Issues, https://github.com/SirCypkowskyy/ExactDocs/issues
7
+ Author: Cyprian Gburek
8
+ License-Expression: Apache-2.0
9
+ Keywords: ai-agents,documentation,fastmcp,mcp,rag
10
+ Classifier: Development Status :: 2 - Pre-Alpha
11
+ Classifier: Framework :: FastAPI
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: Typing :: Typed
14
+ Requires-Python: >=3.14
15
+ Requires-Dist: cyclopts>=4
16
+ Requires-Dist: exactdocs-kernel==0.1.0
17
+ Requires-Dist: fastapi>=0.120
18
+ Requires-Dist: fastmcp>=4
19
+ Requires-Dist: mcp>=2
20
+ Requires-Dist: pydantic-settings>=2.11
21
+ Requires-Dist: pydantic>=2.12
22
+ Requires-Dist: uvicorn[standard]>=0.38
23
+ Description-Content-Type: text/markdown
24
+
25
+ # exactdocs
26
+
27
+ Self-hosted, version-accurate documentation for AI coding agents: an MCP server that serves
28
+ the right docs for the right library version, verbatim and with sources.
29
+
30
+ ```bash
31
+ uvx exactdocs serve # HTTP: web UI + MCP on http://localhost:3000
32
+ uvx exactdocs serve --stdio # MCP over stdio
33
+ ```
34
+
35
+ See the [project README](https://github.com/SirCypkowskyy/ExactDocs) for the full documentation.
@@ -0,0 +1,49 @@
1
+ exactdocs/__init__.py,sha256=ZyWSa5dAmFbfc0TjKWhLJBcrryu1r_VmBOkF37IpkUw,154
2
+ exactdocs/__main__.py,sha256=ssz-NB4QDdynBs1NQKemNmA7m6Qa5FBNfceppQteRJg,49
3
+ exactdocs/_version.py,sha256=auPWjG7MmNQBtyjHzThYGqLlFuaGHu8gyxB7V7-GqvM,218
4
+ exactdocs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ exactdocs/_web/index.html,sha256=qfdyFgXtPPRcBaW1lj4kwVvq7UT4l80uZWtHY6owjSI,394
6
+ exactdocs/_web/assets/index-B350HwjP.js,sha256=xWKuZGMqGuK7_lOFM3oaxffCYQoKzh0CHfU991MIS8Q,366751
7
+ exactdocs/_web/assets/index-CG8rwfz9.css,sha256=foha27uzVRpwi61OcHwCGko46dODPxeFh4UlTWGJlb0,18144
8
+ exactdocs/_web/assets/routes-CCGZxbYl.js,sha256=WYFRFl7Rr2wfuVmqL3NGVtZduzSxr6rgTlCuI15lmTw,71
9
+ exactdocs/bootstrap/__init__.py,sha256=XG_MBkV1HgArcX61rCLHrAFFv3ShMmi-KzpqJX4neJY,192
10
+ exactdocs/bootstrap/bridges.py,sha256=hUcvDHsCKTpMcQJBWaHIWE1tVyZ2i0ZMewR6xFugmr8,1018
11
+ exactdocs/bootstrap/cli.py,sha256=BghXcWdv5ias-ebZPmLH46sj_nM73kJToOHzWQB9wuw,1327
12
+ exactdocs/bootstrap/container.py,sha256=xs5UHI7IcgZiYSbrgEko9t2I9V0lpH5VAhwk7SWsnWI,1092
13
+ exactdocs/bootstrap/http.py,sha256=Fs6E3YWovNlhbqTnaKwNxDkg70rPFAp0x3BKRzvMuho,3349
14
+ exactdocs/bootstrap/mcp.py,sha256=EvoJiroS6fWHldTlKOM2z7qL2nIdI87cRkld7tHCX8A,858
15
+ exactdocs/bootstrap/settings.py,sha256=8-NLbcusqgdvmL2yHvUdjdv75n_EDef8onrtTofbwT8,533
16
+ exactdocs/catalog/__init__.py,sha256=0u3x-I9VxUub0bK5pBNyj1Zt0UmJkPZnPJ_7tD0mnvc,334
17
+ exactdocs/catalog/dtos.py,sha256=uhcxSg_TkFTJa-NrWtD7FETGHXXRDQE9NRdKePR6EHU,1203
18
+ exactdocs/catalog/errors.py,sha256=ClRbwoQJp5nKIWazIVcuAY_LECG0kyiuErewK_bL8R0,464
19
+ exactdocs/catalog/models.py,sha256=neRWNCVD8g6wuLfHRkvLEhgp962OhXBqn7cCbSdAXow,974
20
+ exactdocs/catalog/module.py,sha256=_TNrybsuFmGhIcaQkJhsROKTz4SBePAWL5rOQVGcKdg,958
21
+ exactdocs/catalog/use_cases.py,sha256=JPN-5eqejXXHNJVdggsAxdHqoJ1ebIGALPj2gWadfFM,1159
22
+ exactdocs/catalog/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ exactdocs/catalog/adapters/driven/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ exactdocs/catalog/adapters/driven/memory.py,sha256=Qn1DykuZGf4zPeaMob9QCpGFQbJ1weXroXzWp8p08Ec,1281
25
+ exactdocs/catalog/adapters/driving/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
+ exactdocs/catalog/adapters/driving/http.py,sha256=DJ5c8WUAEUvDKXIp37rwPAEB_y1XkyVE7153l6CYuYI,724
27
+ exactdocs/catalog/adapters/driving/mcp.py,sha256=-lj8sCNL6NRng4Tr83zJ7LtPVsfPLQJgE1FYNCn-OVY,624
28
+ exactdocs/catalog/ports/__init__.py,sha256=RYrj8wC8kMrVVB-gXWv_DSIG5xVW4Wb8db8UFn16Epw,195
29
+ exactdocs/catalog/ports/driven.py,sha256=i0SXcHEC7WM-EzAyOpP3SX5ZpYjlMkicZQ_gwOgOnJY,524
30
+ exactdocs/catalog/ports/driving.py,sha256=qVC4z64AJ7mPDsVRZNqmAOXZpTeD-fLNRIPhJb5Fc2k,597
31
+ exactdocs/retrieval/__init__.py,sha256=OQDJF0SkGRPe9ew8hYdq3o5qBQU03kgI3O81KnwzV-M,250
32
+ exactdocs/retrieval/dtos.py,sha256=TA4Hh9ycwJc8cn5xpEqpolqBF5kwVoAonIb_XAK6qXM,1444
33
+ exactdocs/retrieval/errors.py,sha256=93JiMAB-w46UtYB7GGaq4jArLIstTO3SkbyCKw01dlw,549
34
+ exactdocs/retrieval/models.py,sha256=Cr3X9WYDwH7TAtOVwpaNpep5jGzU6HoPh5nQwGveqqU,874
35
+ exactdocs/retrieval/module.py,sha256=o7V7XnrygWGMba5meh90qWkc73Ax9fyMpFIokh_vgfs,668
36
+ exactdocs/retrieval/use_cases.py,sha256=9rMQb9EqUYLdvPBNnScJ4v5-UhXDOHnl8M5Ve-JRjjw,974
37
+ exactdocs/retrieval/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
+ exactdocs/retrieval/adapters/driven/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
+ exactdocs/retrieval/adapters/driven/memory.py,sha256=ecbyITEWQWAAIIhsc0GPWUcteO_vSyCy5ltY4IlSsLM,1016
40
+ exactdocs/retrieval/adapters/driving/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
+ exactdocs/retrieval/adapters/driving/http.py,sha256=LX33oSBLBqSrTsBeEenD7T7BxLjXtSPMS3u7vAlPG6s,530
42
+ exactdocs/retrieval/adapters/driving/mcp.py,sha256=tD7k79JwWaDRNEOVK8HVUorknlS1nFcgWWMCShwQoco,1139
43
+ exactdocs/retrieval/ports/__init__.py,sha256=Usylx_LGTpaW65iHiPGbj1TIxqo-L2yY5e3qS-T6BBU,193
44
+ exactdocs/retrieval/ports/driven.py,sha256=_mjBPeKbGlWGB1Oo9-wHPeEXYEHQ1mAJhn5RWhwvuDQ,904
45
+ exactdocs/retrieval/ports/driving.py,sha256=3G1OgmLEi-4dGW-H56zYWW_hGz1ZRFlxdENaG3oMhDo,381
46
+ exactdocs-0.1.0.dist-info/METADATA,sha256=anL1Knc8rBlTiM2dWl85EJz8OzCCbMZbweu9WsiSrNQ,1275
47
+ exactdocs-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
48
+ exactdocs-0.1.0.dist-info/entry_points.txt,sha256=OZ7OENarmXQDD7Wl163Y12c8n2DSOTVugwim-Lr0pcs,59
49
+ exactdocs-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ exactdocs = exactdocs.bootstrap.cli:main