mcp-kb 0.2.1__py3-none-any.whl → 0.3.1__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_kb/server/app.py DELETED
@@ -1,210 +0,0 @@
1
- """FastMCP application that exposes knowledge base management tools.
2
-
3
- The module builds a :class:`FastMCP` server configured with the knowledge base
4
- operations defined elsewhere in the package. Using FastMCP drastically reduces
5
- protocol boilerplate because the framework introspects type hints and
6
- Docstrings to generate MCP-compatible tool schemas automatically.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- from dataclasses import dataclass
12
- from typing import Iterable, List
13
-
14
- from mcp.server.fastmcp import FastMCP
15
-
16
- from mcp_kb.config import DOC_FILENAME
17
- from mcp_kb.knowledge.events import (
18
- KnowledgeBaseListener,
19
- KnowledgeBaseSearchListener,
20
- )
21
- from mcp_kb.knowledge.search import build_tree_overview, read_documentation, search_text
22
- from mcp_kb.knowledge.store import FileSegment, KnowledgeBase
23
- from mcp_kb.security.path_validation import PathRules, PathValidationError
24
-
25
-
26
- @dataclass
27
- class ReadFileResult:
28
- """Structured output for the ``kb.read_file`` tool."""
29
-
30
- path: str
31
- start_line: int
32
- end_line: int
33
- content: str
34
-
35
-
36
- @dataclass
37
- class RegexReplaceResult:
38
- """Structured output describing the number of replacements performed."""
39
-
40
- replacements: int
41
-
42
-
43
- @dataclass
44
- class SearchMatchResult:
45
- """Structured representation of a search result with contextual lines."""
46
-
47
- path: str
48
- line: int
49
- context: List[str]
50
-
51
-
52
- def create_fastmcp_app(
53
- rules: PathRules,
54
- *,
55
- host: str | None = None,
56
- port: int | None = None,
57
- listeners: Iterable[KnowledgeBaseListener] | None = None,
58
- ) -> FastMCP:
59
- """Build and return a configured :class:`FastMCP` server instance.
60
-
61
- Parameters
62
- ----------
63
- rules:
64
- Sanitised filesystem rules that restrict all knowledge base operations to
65
- a designated root.
66
- host:
67
- Optional host interface for HTTP/SSE transports. ``None`` uses FastMCP's
68
- defaults.
69
- port:
70
- Optional TCP port for HTTP/SSE transports. ``None`` uses FastMCP's defaults.
71
- listeners:
72
- Optional iterable of :class:`KnowledgeBaseListener` implementations that
73
- should receive change notifications. The iterable is passed directly to
74
- :class:`~mcp_kb.knowledge.store.KnowledgeBase` so that integrations such
75
- as Chroma ingestion can react to file lifecycle events.
76
- """
77
-
78
- kb = KnowledgeBase(rules, listeners=listeners)
79
- search_providers: List[KnowledgeBaseSearchListener] = []
80
- if listeners is not None:
81
- for listener in listeners:
82
- if isinstance(listener, KnowledgeBaseSearchListener):
83
- search_providers.append(listener)
84
- fastmcp_kwargs: dict[str, object] = {}
85
- if host is not None:
86
- fastmcp_kwargs["host"] = host
87
- if port is not None:
88
- fastmcp_kwargs["port"] = port
89
-
90
- mcp = FastMCP(
91
- "mcp-knowledge-base",
92
- instructions=(
93
- "You are connected to a local text-based knowledge base. Use the provided "
94
- "tools to create, inspect, and organize content and search the knowledgebase for information.\n"
95
- "Call the documentation tool first to get the latest documentation."
96
- ),
97
- **fastmcp_kwargs,
98
- )
99
-
100
- @mcp.tool(name="create_file", title="Create File")
101
- def create_file(path: str, content: str) -> str:
102
- """Create or overwrite a text file at ``path`` with ``content``."""
103
-
104
- try:
105
- created = kb.create_file(path, content)
106
- except PathValidationError as exc:
107
- raise ValueError(str(exc)) from exc
108
- return f"Created {created}"
109
-
110
- @mcp.tool(name="read_file", title="Read File", structured_output=True)
111
- def read_file(
112
- path: str, start_line: int | None = None, end_line: int | None = None
113
- ) -> ReadFileResult:
114
- """Read a text file returning metadata about the extracted segment."""
115
-
116
- try:
117
- segment: FileSegment = kb.read_file(
118
- path, start_line=start_line, end_line=end_line
119
- )
120
- except PathValidationError as exc:
121
- raise ValueError(str(exc)) from exc
122
- except FileNotFoundError as exc:
123
- raise ValueError(str(exc)) from exc
124
- return ReadFileResult(
125
- path=str(segment.path),
126
- start_line=segment.start_line,
127
- end_line=segment.end_line,
128
- content=segment.content,
129
- )
130
-
131
- @mcp.tool(name="append_file", title="Append File")
132
- def append_file(path: str, content: str) -> str:
133
- """Append ``content`` to the file specified by ``path``."""
134
-
135
- try:
136
- target = kb.append_file(path, content)
137
- except PathValidationError as exc:
138
- raise ValueError(str(exc)) from exc
139
- return f"Appended to {target}"
140
-
141
- @mcp.tool(name="regex_replace", title="Regex Replace", structured_output=True)
142
- def regex_replace(path: str, pattern: str, replacement: str) -> RegexReplaceResult:
143
- """Perform a regex-based replacement across the full file."""
144
-
145
- try:
146
- replacements = kb.regex_replace(path, pattern, replacement)
147
- except PathValidationError as exc:
148
- raise ValueError(str(exc)) from exc
149
- return RegexReplaceResult(replacements=replacements)
150
-
151
- @mcp.tool(name="delete", title="Soft Delete")
152
- def delete(path: str) -> str:
153
- """Soft delete the file at ``path`` by appending the configured sentinel."""
154
-
155
- try:
156
- deleted = kb.soft_delete(path)
157
- except PathValidationError as exc:
158
- raise ValueError(str(exc)) from exc
159
- except FileNotFoundError as exc:
160
- raise ValueError(str(exc)) from exc
161
- return f"Marked {deleted.name} as deleted"
162
-
163
- @mcp.tool(name="search", title="Search", structured_output=True)
164
- def search(query: str, limit: int = 5) -> List[SearchMatchResult]:
165
- """Search for ``query`` across the knowledge base with semantic ranking.
166
-
167
- Registered listeners that implement the optional search interface are
168
- queried first (e.g., the Chroma ingestor). When no listener returns a
169
- result the tool falls back to streaming the markdown files directly so
170
- callers always receive deterministic text snippets.
171
- """
172
-
173
- if limit <= 0:
174
- raise ValueError("limit must be greater than zero")
175
-
176
- matches = search_text(
177
- kb,
178
- query,
179
- providers=search_providers,
180
- n_results=limit,
181
- )
182
- return [
183
- SearchMatchResult(
184
- path=str(
185
- match.path.relative_to(kb.rules.root)
186
- if match.path.is_absolute()
187
- else match.path
188
- ),
189
- line=match.line_number,
190
- context=match.context,
191
- )
192
- for match in matches
193
- ]
194
-
195
- @mcp.tool(name="overview", title="Overview")
196
- def overview() -> str:
197
- """Return a textual tree describing the knowledge base structure."""
198
-
199
- return build_tree_overview(kb)
200
-
201
- @mcp.tool(name="documentation", title="Documentation")
202
- def documentation() -> str:
203
- """Read the knowledge base documentation if ``%s`` exists.""" % DOC_FILENAME
204
-
205
- text = read_documentation(kb)
206
- if not text:
207
- return "Documentation is not available."
208
- return text
209
-
210
- return mcp
mcp_kb/utils/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Utility helpers shared across the knowledge base server modules."""
@@ -1,128 +0,0 @@
1
- """Filesystem helpers wrapping Python's standard library primitives.
2
-
3
- The knowledge base server performs numerous file operations. Consolidating the
4
- logic in this module keeps the rest of the code focused on business semantics
5
- such as validating incoming requests and shaping responses. Each helper function
6
- is intentionally small so that callers can compose them for different workflows
7
- without duplicating the low-level boilerplate.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- from contextlib import contextmanager
13
- from pathlib import Path
14
- from threading import Lock
15
- from typing import Dict, Iterator
16
-
17
-
18
- class FileLockRegistry:
19
- """In-memory lock registry to serialize write operations per file.
20
-
21
- Using per-path locks prevents concurrent writes from interleaving content
22
- and potentially corrupting files. The registry lazily creates locks when a
23
- path is first encountered. We reuse locks for subsequent operations to avoid
24
- unbounded memory usage.
25
- """
26
-
27
- def __init__(self) -> None:
28
- """Initialize the registry with an empty dictionary."""
29
-
30
- self._locks: Dict[Path, Lock] = {}
31
- self._global_lock = Lock()
32
-
33
- @contextmanager
34
- def acquire(self, path: Path) -> Iterator[None]:
35
- """Context manager that acquires a lock for the supplied path.
36
-
37
- The helper nests two locks: a global mutex to retrieve or create the
38
- per-path lock, and the per-path lock itself for the duration of the
39
- caller's critical section.
40
-
41
- Parameters
42
- ----------
43
- path:
44
- Absolute path indicating which file should be protected.
45
- """
46
-
47
- with self._global_lock:
48
- lock = self._locks.setdefault(path, Lock())
49
- lock.acquire()
50
- try:
51
- yield
52
- finally:
53
- lock.release()
54
-
55
-
56
- def write_text(path: Path, content: str) -> None:
57
- """Write text content to ``path`` using UTF-8 encoding."""
58
-
59
- path.write_text(content, encoding="utf-8")
60
-
61
-
62
- def append_text(path: Path, content: str) -> None:
63
- """Append text content to ``path`` using UTF-8 encoding."""
64
-
65
- with path.open("a", encoding="utf-8") as handle:
66
- handle.write(content)
67
-
68
-
69
- def read_text(path: Path) -> str:
70
- """Read UTF-8 text content from ``path`` and return it."""
71
-
72
- return path.read_text(encoding="utf-8")
73
-
74
-
75
- def ensure_parent_directory(path: Path) -> None:
76
- """Ensure the parent directory of ``path`` exists by creating it."""
77
-
78
- path.parent.mkdir(parents=True, exist_ok=True)
79
-
80
-
81
- def rename(path: Path, target: Path) -> None:
82
- """Rename ``path`` to ``target`` using ``Path.rename`` semantics."""
83
-
84
- path.rename(target)
85
-
86
-
87
- def is_text_file(path: Path, max_bytes: int = 2048) -> bool:
88
- """Heuristically determine whether ``path`` contains UTF-8 text.
89
-
90
- The check is designed to be fast and conservative for use when iterating
91
- a directory tree. It reads at most ``max_bytes`` from the file in binary
92
- mode and applies two filters:
93
-
94
- - Reject files that contain NUL bytes, which are extremely uncommon in
95
- textual formats and a strong indicator of binary content.
96
- - Attempt to decode the sampled bytes as UTF-8. If decoding fails, the
97
- file is treated as binary.
98
-
99
- Parameters
100
- ----------
101
- path:
102
- Absolute path to the file on disk.
103
- max_bytes:
104
- Upper bound on the number of bytes to sample from the head of the
105
- file. A small sample keeps directory scans fast while remaining
106
- accurate for typical text formats such as ``.md``, ``.txt``, ``.xml``,
107
- and source files.
108
-
109
- Returns
110
- -------
111
- bool
112
- ``True`` if the file appears to be UTF-8 text; ``False`` otherwise.
113
- """
114
-
115
- try:
116
- with path.open("rb") as handle:
117
- sample = handle.read(max_bytes)
118
- except (FileNotFoundError, PermissionError): # pragma: no cover - defensive
119
- return False
120
-
121
- if b"\x00" in sample:
122
- return False
123
-
124
- try:
125
- sample.decode("utf-8")
126
- return True
127
- except UnicodeDecodeError:
128
- return False
@@ -1,26 +0,0 @@
1
- mcp_kb/__init__.py,sha256=Ry7qODhfFQF6u6p2m3bwGWhB0-BdWTQcHDJB7NBYAio,74
2
- mcp_kb/config.py,sha256=NUpzjDH4PQw4FyjGgYUcMsGMeenNiZTMrQj4U62xKlk,2530
3
- mcp_kb/cli/__init__.py,sha256=dEIRWFycAfPkha1S1Bj_Y6zkvEZv4eF0qtbF9t74r60,67
4
- mcp_kb/cli/args.py,sha256=YVO7teHGuk2Yc36Sqwtv457dgrRd_YB7YN5wwFLKSXs,5371
5
- mcp_kb/cli/main.py,sha256=twwBKRyS21IPolkLyM2gDvFBko3QpR9_ho-NJ1uXcR4,3912
6
- mcp_kb/cli/reindex.py,sha256=TAcBtjsEJ1wSyB8iOUWp8I7PHVOEuNFMe0-Mc7mk21Y,3047
7
- mcp_kb/data/KNOWLEDBASE_DOC.md,sha256=lZqoSRQuIs7nN0UD5GJOnE6B7XvU3gywrBoY6ToK-pE,6582
8
- mcp_kb/data/__init__.py,sha256=UYYuO_n2ikjpwkPSykgleiifYvC0V8_O-atUaRBQUm4,70
9
- mcp_kb/ingest/__init__.py,sha256=8obrvfa8nLNLYPbi1MHlFUqfoFHgK9YfdryPzAXQ6kU,77
10
- mcp_kb/ingest/chroma.py,sha256=KCNySeUpCy-FEHglacE1hh8WpgcDqbZ-UTTkFChFAuA,22436
11
- mcp_kb/knowledge/__init__.py,sha256=W_dtRbtnQlrDJ_425vWR8BcoZGJ8gC5-wg1De1E654s,76
12
- mcp_kb/knowledge/bootstrap.py,sha256=Og72GvxeJX7PLe_vHVMzRqnXIS06JswSrIdKcz776_8,1237
13
- mcp_kb/knowledge/events.py,sha256=V-64uBbJZdKm8mwcbeOMtSC8VZW5NoN1DwUtcwfOFVc,3550
14
- mcp_kb/knowledge/search.py,sha256=Qx2AxuQ1h0hdOMX9du9I6-yStzOlvIiB9d7F6fFGOf4,5908
15
- mcp_kb/knowledge/store.py,sha256=eTBtCTTkyGizXoEz4cO_YGUmR26fmUXhyCSLrfZ3-CY,10021
16
- mcp_kb/security/__init__.py,sha256=lF8_XAjzpwhAFresuskXMo0u9v7KFiTJId88wqOAM4Y,62
17
- mcp_kb/security/path_validation.py,sha256=3f-0De4801-cMU4uwi1QM6NalAj4IU_hj8r-iK0NJ_k,3662
18
- mcp_kb/server/__init__.py,sha256=j9TmxW_WLCoibyQvCsDT1MIuUqSL8sRh2h4u0M4eU0c,74
19
- mcp_kb/server/app.py,sha256=KAnOXT-7TdKqt_uW_vobc6RLq8YWRzahTQSGl8vGZy8,7287
20
- mcp_kb/utils/__init__.py,sha256=lKhRsjgnbhye1sSlch1_wsAI3eWKE1M6RVIiNlnsvLI,71
21
- mcp_kb/utils/filesystem.py,sha256=1Jr9cxIimV-o91DJMh5lR9GLFE3BDknoGquVBFQ-fd4,4027
22
- mcp_kb-0.2.1.dist-info/METADATA,sha256=53sKD_Z4cBkBFsCC36giXr4LsJGyoPMCTroHez2XZi4,5122
23
- mcp_kb-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
- mcp_kb-0.2.1.dist-info/entry_points.txt,sha256=qwJkR3vV7ZeydfS_IYMiDwLv4BdTkrOf4-5neWj25g0,96
25
- mcp_kb-0.2.1.dist-info/top_level.txt,sha256=IBiz3TNE3FF3TwkbCZpC1kkk6ohTwtBQNSPJNV3-qGA,7
26
- mcp_kb-0.2.1.dist-info/RECORD,,
File without changes