mcp-kb 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_kb/cli/__init__.py +1 -0
- mcp_kb/cli/args.py +168 -0
- mcp_kb/cli/main.py +175 -0
- mcp_kb/cli/reindex.py +113 -0
- mcp_kb/cli/runtime_config.py +421 -0
- mcp_kb/data/KNOWLEDBASE_DOC.md +151 -0
- mcp_kb/data/__init__.py +1 -0
- mcp_kb/ingest/__init__.py +1 -0
- mcp_kb/ingest/chroma.py +1287 -0
- mcp_kb/knowledge/__init__.py +1 -0
- mcp_kb/knowledge/bootstrap.py +44 -0
- mcp_kb/knowledge/events.py +105 -0
- mcp_kb/knowledge/search.py +177 -0
- mcp_kb/knowledge/store.py +294 -0
- mcp_kb/security/__init__.py +1 -0
- mcp_kb/security/path_validation.py +108 -0
- mcp_kb/server/__init__.py +1 -0
- mcp_kb/server/app.py +201 -0
- mcp_kb/ui/__init__.py +17 -0
- mcp_kb/ui/api.py +377 -0
- mcp_kb/ui/assets/assets/index.css +1 -0
- mcp_kb/ui/assets/index.html +62 -0
- mcp_kb/ui/server.py +332 -0
- mcp_kb/utils/__init__.py +1 -0
- mcp_kb/utils/filesystem.py +128 -0
- mcp_kb-0.3.2.dist-info/METADATA +338 -0
- mcp_kb-0.3.2.dist-info/RECORD +32 -0
- {mcp_kb-0.3.0.dist-info → mcp_kb-0.3.2.dist-info}/entry_points.txt +1 -0
- mcp_kb-0.3.0.dist-info/METADATA +0 -178
- mcp_kb-0.3.0.dist-info/RECORD +0 -7
- {mcp_kb-0.3.0.dist-info → mcp_kb-0.3.2.dist-info}/WHEEL +0 -0
- {mcp_kb-0.3.0.dist-info → mcp_kb-0.3.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,108 @@
|
|
1
|
+
"""Path validation utilities to protect the knowledge base filesystem.
|
2
|
+
|
3
|
+
This module implements reusable helpers that ensure every file operation stays
|
4
|
+
inside the configured knowledge base root. The checks defend against directory
|
5
|
+
traversal attempts (".." components), accidental absolute paths, and writes
|
6
|
+
that target the reserved documentation folder. The helper functions are written
|
7
|
+
so they can be reused both by the server runtime and by unit tests to keep the
|
8
|
+
security rules consistent.
|
9
|
+
"""
|
10
|
+
|
11
|
+
from __future__ import annotations
|
12
|
+
|
13
|
+
from pathlib import Path
|
14
|
+
from typing import Iterable
|
15
|
+
from pydantic import BaseModel
|
16
|
+
|
17
|
+
from mcp_kb.config import DATA_FOLDER_NAME, DELETE_SENTINEL
|
18
|
+
|
19
|
+
|
20
|
+
class PathValidationError(ValueError):
|
21
|
+
"""Error raised when a path fails validation rules.
|
22
|
+
|
23
|
+
The server treats any instance of this exception as a client error: callers
|
24
|
+
attempted to access a disallowed path. Raising a dedicated subclass of
|
25
|
+
``ValueError`` enables precise error handling and cleaner unit tests.
|
26
|
+
"""
|
27
|
+
|
28
|
+
|
29
|
+
class PathRules(BaseModel):
|
30
|
+
"""Container for server-specific path constraints.
|
31
|
+
|
32
|
+
Attributes
|
33
|
+
----------
|
34
|
+
root:
|
35
|
+
Absolute path that represents the root of the knowledge base. All file
|
36
|
+
operations must remain inside this directory tree.
|
37
|
+
protected_folders:
|
38
|
+
Iterable of folder names that are protected against mutations. The
|
39
|
+
server uses this to forbid modifications to the documentation folder
|
40
|
+
while still allowing read operations.
|
41
|
+
"""
|
42
|
+
root: Path
|
43
|
+
protected_folders: Iterable[str]
|
44
|
+
|
45
|
+
|
46
|
+
def normalize_path(candidate: Union[str, Path], rules: PathRules) -> Path:
|
47
|
+
"""Normalize a relative path and ensure it stays inside the root.
|
48
|
+
|
49
|
+
Parameters
|
50
|
+
----------
|
51
|
+
candidate:
|
52
|
+
The user-provided path, typically originating from an MCP tool request.
|
53
|
+
rules:
|
54
|
+
The active ``PathRules`` instance describing allowed operations.
|
55
|
+
|
56
|
+
Returns
|
57
|
+
-------
|
58
|
+
Path
|
59
|
+
A fully-resolved path that is guaranteed to be inside the root
|
60
|
+
directory.
|
61
|
+
|
62
|
+
Raises
|
63
|
+
------
|
64
|
+
PathValidationError
|
65
|
+
If the candidate path is absolute, attempts traversal outside the root,
|
66
|
+
or resolves to a location that is not within the permitted tree.
|
67
|
+
"""
|
68
|
+
|
69
|
+
path_obj = Path(candidate)
|
70
|
+
if path_obj.is_absolute():
|
71
|
+
raise PathValidationError(
|
72
|
+
"Absolute paths are not permitted inside the knowledge base"
|
73
|
+
)
|
74
|
+
|
75
|
+
normalized = (rules.root / path_obj).resolve()
|
76
|
+
try:
|
77
|
+
normalized.relative_to(rules.root)
|
78
|
+
except ValueError as exc:
|
79
|
+
raise PathValidationError(
|
80
|
+
"Path resolves outside the knowledge base root"
|
81
|
+
) from exc
|
82
|
+
|
83
|
+
if DELETE_SENTINEL in normalized.name:
|
84
|
+
raise PathValidationError("Operations on soft-deleted files are not permitted")
|
85
|
+
|
86
|
+
return normalized
|
87
|
+
|
88
|
+
|
89
|
+
def ensure_write_allowed(path: Path, rules: PathRules) -> None:
|
90
|
+
"""Validate that a path resides outside protected folders before writing.
|
91
|
+
|
92
|
+
The function raises a ``PathValidationError`` when the path is located
|
93
|
+
inside one of the configured protected folders. Read operations can still
|
94
|
+
access those directories by skipping this check.
|
95
|
+
|
96
|
+
Parameters
|
97
|
+
----------
|
98
|
+
path:
|
99
|
+
The already-normalized absolute path that will be used for writing.
|
100
|
+
rules:
|
101
|
+
The active ``PathRules`` instance describing allowed operations.
|
102
|
+
"""
|
103
|
+
|
104
|
+
relative_parts = path.relative_to(rules.root).parts
|
105
|
+
if relative_parts and relative_parts[0] in set(rules.protected_folders):
|
106
|
+
raise PathValidationError(
|
107
|
+
f"Writes are not allowed inside the protected folder '{relative_parts[0]}'"
|
108
|
+
)
|
@@ -0,0 +1 @@
|
|
1
|
+
"""Server subpackage powering the FastMCP-based knowledge base server."""
|
mcp_kb/server/app.py
ADDED
@@ -0,0 +1,201 @@
|
|
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 typing import Iterable, List
|
12
|
+
from pydantic import BaseModel
|
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
|
+
class ReadFileResult(BaseModel):
|
27
|
+
"""Structured output for the ``kb.read_file`` tool."""
|
28
|
+
|
29
|
+
path: str
|
30
|
+
start_line: int
|
31
|
+
end_line: int
|
32
|
+
content: str
|
33
|
+
|
34
|
+
|
35
|
+
class RegexReplaceResult(BaseModel):
|
36
|
+
"""Structured output describing the number of replacements performed."""
|
37
|
+
|
38
|
+
replacements: int
|
39
|
+
|
40
|
+
|
41
|
+
|
42
|
+
def create_fastmcp_app(
|
43
|
+
rules: PathRules,
|
44
|
+
*,
|
45
|
+
host: str | None = None,
|
46
|
+
port: int | None = None,
|
47
|
+
listeners: Iterable[KnowledgeBaseListener] | None = None,
|
48
|
+
) -> FastMCP:
|
49
|
+
"""Build and return a configured :class:`FastMCP` server instance.
|
50
|
+
|
51
|
+
Parameters
|
52
|
+
----------
|
53
|
+
rules:
|
54
|
+
Sanitised filesystem rules that restrict all knowledge base operations to
|
55
|
+
a designated root.
|
56
|
+
host:
|
57
|
+
Optional host interface for HTTP/SSE transports. ``None`` uses FastMCP's
|
58
|
+
defaults.
|
59
|
+
port:
|
60
|
+
Optional TCP port for HTTP/SSE transports. ``None`` uses FastMCP's defaults.
|
61
|
+
listeners:
|
62
|
+
Optional iterable of :class:`KnowledgeBaseListener` implementations that
|
63
|
+
should receive change notifications. The iterable is passed directly to
|
64
|
+
:class:`~mcp_kb.knowledge.store.KnowledgeBase` so that integrations such
|
65
|
+
as Chroma ingestion can react to file lifecycle events.
|
66
|
+
"""
|
67
|
+
|
68
|
+
kb = KnowledgeBase(rules, listeners=listeners)
|
69
|
+
search_providers: List[KnowledgeBaseSearchListener] = []
|
70
|
+
if listeners is not None:
|
71
|
+
for listener in listeners:
|
72
|
+
if isinstance(listener, KnowledgeBaseSearchListener):
|
73
|
+
search_providers.append(listener)
|
74
|
+
fastmcp_kwargs: dict[str, object] = {}
|
75
|
+
if host is not None:
|
76
|
+
fastmcp_kwargs["host"] = host
|
77
|
+
if port is not None:
|
78
|
+
fastmcp_kwargs["port"] = port
|
79
|
+
|
80
|
+
mcp = FastMCP(
|
81
|
+
"mcp-knowledge-base",
|
82
|
+
instructions=(
|
83
|
+
"You are connected to a local text-based knowledge base. Use the provided "
|
84
|
+
"tools to create, inspect, and organize content and search the knowledgebase for information.\n"
|
85
|
+
"Call the documentation tool first to get the latest documentation."
|
86
|
+
),
|
87
|
+
**fastmcp_kwargs,
|
88
|
+
)
|
89
|
+
|
90
|
+
# Attach the knowledge base to the FastMCP instance for reuse by
|
91
|
+
# auxiliary servers (e.g., the human UI HTTP server). FastMCP does not
|
92
|
+
# expose a public extension API for storing arbitrary state, but keeping a
|
93
|
+
# direct attribute is harmless and enables tight coupling where needed.
|
94
|
+
# Downstream code should treat this as best-effort and feature-gated.
|
95
|
+
setattr(mcp, "kb", kb) # type: ignore[attr-defined]
|
96
|
+
|
97
|
+
@mcp.tool(name="create_file", title="Create File")
|
98
|
+
def create_file(path: str, content: str) -> str:
|
99
|
+
"""Create or overwrite a text file at ``path`` with ``content``."""
|
100
|
+
|
101
|
+
try:
|
102
|
+
created = kb.create_file(path, content)
|
103
|
+
except PathValidationError as exc:
|
104
|
+
raise ValueError(str(exc)) from exc
|
105
|
+
return f"Created {created}"
|
106
|
+
|
107
|
+
@mcp.tool(name="read_file", title="Read File", structured_output=True)
|
108
|
+
def read_file(
|
109
|
+
path: str, start_line: int | None = None, end_line: int | None = None
|
110
|
+
) -> ReadFileResult:
|
111
|
+
"""Read a text file returning metadata about the extracted segment.
|
112
|
+
start_line and end_line are 0-based line numbers.
|
113
|
+
"""
|
114
|
+
|
115
|
+
try:
|
116
|
+
segment: FileSegment = kb.read_file(
|
117
|
+
path, start_line=start_line, end_line=end_line
|
118
|
+
)
|
119
|
+
except PathValidationError as exc:
|
120
|
+
raise ValueError(str(exc)) from exc
|
121
|
+
except FileNotFoundError as exc:
|
122
|
+
raise ValueError(str(exc)) from exc
|
123
|
+
return ReadFileResult(
|
124
|
+
path=str(segment.path),
|
125
|
+
start_line=segment.start_line,
|
126
|
+
end_line=segment.end_line,
|
127
|
+
content=segment.content,
|
128
|
+
)
|
129
|
+
|
130
|
+
@mcp.tool(name="append_file", title="Append File")
|
131
|
+
def append_file(path: str, content: str) -> str:
|
132
|
+
"""Append ``content`` to the file specified by ``path``."""
|
133
|
+
|
134
|
+
try:
|
135
|
+
target = kb.append_file(path, content)
|
136
|
+
except PathValidationError as exc:
|
137
|
+
raise ValueError(str(exc)) from exc
|
138
|
+
return f"Appended to {target}"
|
139
|
+
|
140
|
+
@mcp.tool(name="regex_replace", title="Regex Replace", structured_output=True)
|
141
|
+
def regex_replace(path: str, pattern: str, replacement: str) -> RegexReplaceResult:
|
142
|
+
"""Perform a regex-based replacement across the full file."""
|
143
|
+
|
144
|
+
try:
|
145
|
+
replacements = kb.regex_replace(path, pattern, replacement)
|
146
|
+
except PathValidationError as exc:
|
147
|
+
raise ValueError(str(exc)) from exc
|
148
|
+
return RegexReplaceResult(replacements=replacements)
|
149
|
+
|
150
|
+
@mcp.tool(name="delete", title="Soft Delete")
|
151
|
+
def delete(path: str) -> str:
|
152
|
+
"""Soft delete the file at ``path`` by appending the configured sentinel."""
|
153
|
+
|
154
|
+
try:
|
155
|
+
deleted = kb.soft_delete(path)
|
156
|
+
except PathValidationError as exc:
|
157
|
+
raise ValueError(str(exc)) from exc
|
158
|
+
except FileNotFoundError as exc:
|
159
|
+
raise ValueError(str(exc)) from exc
|
160
|
+
return f"Marked {deleted.name} as deleted"
|
161
|
+
|
162
|
+
@mcp.tool(name="search", title="Search", structured_output=True)
|
163
|
+
def search(query: str, limit: int = 5) -> List[FileSegment]:
|
164
|
+
"""Search for ``query`` across the knowledge base with semantic ranking.
|
165
|
+
|
166
|
+
Registered listeners that implement the optional search interface are
|
167
|
+
queried first (e.g., the Chroma ingestor). When no listener returns a
|
168
|
+
result the tool falls back to streaming the markdown files directly so
|
169
|
+
callers always receive deterministic text snippets.
|
170
|
+
"""
|
171
|
+
|
172
|
+
if limit <= 0:
|
173
|
+
raise ValueError("limit must be greater than zero")
|
174
|
+
|
175
|
+
matches = search_text(
|
176
|
+
kb,
|
177
|
+
query,
|
178
|
+
providers=search_providers,
|
179
|
+
n_results=limit,
|
180
|
+
)[0]
|
181
|
+
|
182
|
+
for match in matches:
|
183
|
+
match.assert_path(kb.rules)
|
184
|
+
return matches
|
185
|
+
|
186
|
+
@mcp.tool(name="overview", title="Overview")
|
187
|
+
def overview() -> str:
|
188
|
+
"""Return a textual tree describing the knowledge base structure."""
|
189
|
+
|
190
|
+
return build_tree_overview(kb)
|
191
|
+
|
192
|
+
@mcp.tool(name="documentation", title="Documentation")
|
193
|
+
def documentation() -> str:
|
194
|
+
"""Read the knowledge base documentation if ``%s`` exists.""" % DOC_FILENAME
|
195
|
+
|
196
|
+
text = read_documentation(kb)
|
197
|
+
if not text:
|
198
|
+
return "Documentation is not available."
|
199
|
+
return text
|
200
|
+
|
201
|
+
return mcp
|
mcp_kb/ui/__init__.py
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
"""Human-accessible UI for browsing and editing the knowledge base.
|
2
|
+
|
3
|
+
This package exposes a tiny HTTP server using Python's standard library so we
|
4
|
+
avoid introducing new runtime dependencies. The server mounts a minimal web UI
|
5
|
+
with a menu bar and a "Browse" view that renders a file tree and a simple text
|
6
|
+
editor. Changes are persisted via the same :class:`~mcp_kb.knowledge.store.KnowledgeBase`
|
7
|
+
instance as the MCP server, ensuring that all registered listeners and
|
8
|
+
triggers execute uniformly regardless of the entry point.
|
9
|
+
"""
|
10
|
+
|
11
|
+
from __future__ import annotations
|
12
|
+
|
13
|
+
__all__ = [
|
14
|
+
"start_ui_server",
|
15
|
+
]
|
16
|
+
|
17
|
+
from .server import start_ui_server
|