mcp-server-lore 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.
- lore/__init__.py +1 -0
- lore/core/__init__.py +1 -0
- lore/core/constants.py +62 -0
- lore/core/scanner.py +208 -0
- lore/mcp/__init__.py +1 -0
- lore/mcp/backends/__init__.py +1 -0
- lore/mcp/backends/gists.py +467 -0
- lore/mcp/backends/gists_client.py +493 -0
- lore/mcp/embeddings.py +107 -0
- lore/mcp/models.py +100 -0
- lore/mcp/router.py +574 -0
- lore/mcp/server.py +330 -0
- lore/seed/__init__.py +9 -0
- lore/seed/concepts.py +416 -0
- lore/selfhosted/Dockerfile +88 -0
- lore/selfhosted/__init__.py +1 -0
- lore/selfhosted/db.py +435 -0
- lore/selfhosted/entrypoint.sh +15 -0
- lore/selfhosted/indexer.py +164 -0
- lore/selfhosted/schema.sql +51 -0
- lore/selfhosted/vector_store.py +181 -0
- lore/semantic_server/Dockerfile +76 -0
- lore/semantic_server/__init__.py +1 -0
- lore/semantic_server/entrypoint.sh +24 -0
- lore/server/__init__.py +1 -0
- lore/server/api.py +1012 -0
- lore/server/auth.py +133 -0
- lore/server/storage/__init__.py +5 -0
- lore/server/storage/base.py +139 -0
- lore/server/storage/gist_qdrant.py +471 -0
- lore/server/storage/sqlite_qdrant.py +393 -0
- lore/server/watcher.py +637 -0
- lore/tests/__init__.py +1 -0
- lore/tests/conftest.py +12 -0
- lore/tests/test_api.py +765 -0
- lore/tests/test_auth.py +613 -0
- lore/tests/test_benchmark_gists.py +348 -0
- lore/tests/test_benchmark_noise.py +217 -0
- lore/tests/test_db.py +389 -0
- lore/tests/test_dedup.py +710 -0
- lore/tests/test_embeddings.py +115 -0
- lore/tests/test_gists_backend.py +529 -0
- lore/tests/test_gists_client.py +377 -0
- lore/tests/test_gists_link.py +430 -0
- lore/tests/test_gists_rate.py +530 -0
- lore/tests/test_gists_search.py +563 -0
- lore/tests/test_indexer.py +309 -0
- lore/tests/test_live_registration.py +97 -0
- lore/tests/test_mcp_server.py +599 -0
- lore/tests/test_plugin_wiring.py +353 -0
- lore/tests/test_ratings.py +860 -0
- lore/tests/test_router.py +563 -0
- lore/tests/test_scanner.py +274 -0
- lore/tests/test_seed.py +261 -0
- lore/tests/test_semantic_api.py +737 -0
- lore/tests/test_storage_backend.py +506 -0
- lore/tests/test_vector_store.py +277 -0
- lore/tests/test_watcher.py +735 -0
- mcp_server_lore-0.1.0.dist-info/METADATA +17 -0
- mcp_server_lore-0.1.0.dist-info/RECORD +62 -0
- mcp_server_lore-0.1.0.dist-info/WHEEL +4 -0
- mcp_server_lore-0.1.0.dist-info/entry_points.txt +2 -0
lore/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Lore — typed, linked knowledge graph for AI coding agents.
|
lore/core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Lore core — cross-cutting utilities shared across backends."""
|
lore/core/constants.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Shared constants and helpers used across multiple Lore modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
VALID_LINK_RELS: frozenset[str] = frozenset(
|
|
6
|
+
{"uses", "tested_by", "extends", "alternative_to", "requires"}
|
|
7
|
+
)
|
|
8
|
+
VALID_CONCEPT_TYPES: frozenset[str] = frozenset(
|
|
9
|
+
{"project", "pattern", "tool", "testing", "architecture"}
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _parse_name_from_description(description: str) -> str:
|
|
14
|
+
"""Parse a lore-concept gist description: '[agentlore-concept] Name [tag1,tag2]'.
|
|
15
|
+
|
|
16
|
+
Input format: ``"[agentlore-concept] My Pattern [tag1, tag2]"``
|
|
17
|
+
Output: ``"My Pattern"``
|
|
18
|
+
|
|
19
|
+
Strips the leading ``"[agentlore-concept] "`` prefix, then strips the trailing
|
|
20
|
+
``" [...]"`` suffix (everything from the last ``" ["`` onwards). If
|
|
21
|
+
parsing fails for any reason the full description is returned as a fallback.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
description: The raw gist description string.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
The extracted concept name, or the full description if parsing fails.
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
prefix = "[agentlore-concept] "
|
|
31
|
+
if not description.startswith(prefix):
|
|
32
|
+
return description
|
|
33
|
+
remainder = description[len(prefix):]
|
|
34
|
+
# Strip trailing " [...]" tag section if present.
|
|
35
|
+
last_bracket = remainder.rfind(" [")
|
|
36
|
+
if last_bracket != -1:
|
|
37
|
+
remainder = remainder[:last_bracket]
|
|
38
|
+
return remainder
|
|
39
|
+
except Exception: # noqa: BLE001
|
|
40
|
+
return description
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def embedding_text(when_to_use: str, name: str) -> str:
|
|
44
|
+
"""Return the canonical embedding input string for a concept.
|
|
45
|
+
|
|
46
|
+
All backends must use this function to ensure vectors are in the same
|
|
47
|
+
embedding space. The canonical form is ``when_to_use + " " + name``.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
when_to_use: The concept's when_to_use field.
|
|
51
|
+
name: The concept's name field.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
A single string ready to pass to the embedding model.
|
|
55
|
+
"""
|
|
56
|
+
return (when_to_use or "") + " " + (name or "")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
#: Cosine-similarity threshold above which two concepts are considered
|
|
60
|
+
#: near-duplicates. Can be overridden at request time via the
|
|
61
|
+
#: ``LORE_DEDUP_THRESHOLD`` environment variable.
|
|
62
|
+
LORE_DEDUP_THRESHOLD: float = 0.92
|
lore/core/scanner.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Content scanner for the Lore MCP server (LORE-005).
|
|
2
|
+
|
|
3
|
+
Scans concept fields for credentials, internal URLs, and custom blocklist
|
|
4
|
+
patterns before any write to the knowledge graph.
|
|
5
|
+
|
|
6
|
+
Environment variables
|
|
7
|
+
---------------------
|
|
8
|
+
LORE_BLOCK_PATTERNS
|
|
9
|
+
Semicolon-separated list of additional regex patterns to block. Evaluated
|
|
10
|
+
at import time so the list is compiled once per process. Example::
|
|
11
|
+
|
|
12
|
+
LORE_BLOCK_PATTERNS=company\\.internal;secret-project
|
|
13
|
+
|
|
14
|
+
Pattern catalogue
|
|
15
|
+
-----------------
|
|
16
|
+
1. **Credential patterns** — API keys, tokens, bearer strings, secrets.
|
|
17
|
+
2. **Long hex / base64 strings** — raw secrets without a label.
|
|
18
|
+
3. **Internal URL patterns** — ``localhost`` (non-example), ``.internal``,
|
|
19
|
+
``.corp``, ``.local`` TLD suffixes.
|
|
20
|
+
4. **Custom blocklist** — compiled from ``LORE_BLOCK_PATTERNS`` at startup.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
import re
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Built-in pattern definitions
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
# Credential label + value.
|
|
34
|
+
# Handles two forms:
|
|
35
|
+
# - key=value / key: value (api_key, token, secret, bearer)
|
|
36
|
+
# - "Bearer <token>" as used in Authorization headers (space separator)
|
|
37
|
+
_CREDENTIAL_LABEL_RE = re.compile(
|
|
38
|
+
r"(?i)"
|
|
39
|
+
r"(?:"
|
|
40
|
+
r"(api[_\-]?key|token|secret)\s*[:=]\s*\S{8,}" # key/token/secret + colon/equals
|
|
41
|
+
r"|"
|
|
42
|
+
r"bearer\s*[:=\s]\s*\S{8,}" # Bearer: / Bearer= / Bearer <value>
|
|
43
|
+
r")"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Long hex string (32+ chars) — raw secret without a label
|
|
47
|
+
_LONG_HEX_RE = re.compile(r"[A-Fa-f0-9]{32,}")
|
|
48
|
+
|
|
49
|
+
# Long base64-ish string (40+ chars) — could be a token or private key chunk
|
|
50
|
+
_LONG_B64_RE = re.compile(r"[A-Za-z0-9+/]{40,}=*")
|
|
51
|
+
|
|
52
|
+
# Internal URL patterns:
|
|
53
|
+
# - bare "localhost" that is NOT inside an obvious example/placeholder string
|
|
54
|
+
# - hostnames ending in .internal, .corp, or .local
|
|
55
|
+
_INTERNAL_HOST_RE = re.compile(
|
|
56
|
+
r"(?i)"
|
|
57
|
+
r"(?<!['\"])\blocalhost\b(?!['\"])" # localhost not inside quotes
|
|
58
|
+
r"|"
|
|
59
|
+
r"\.(internal|corp|local)\b"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _compile_block_patterns() -> list[re.Pattern[str]]:
|
|
64
|
+
"""Compile custom blocklist patterns from ``LORE_BLOCK_PATTERNS`` env var.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
A list of compiled :class:`re.Pattern` objects. Invalid patterns are
|
|
68
|
+
skipped with a warning printed to stderr so a misconfigured pattern
|
|
69
|
+
does not crash the server at startup.
|
|
70
|
+
"""
|
|
71
|
+
raw = os.environ.get("LORE_BLOCK_PATTERNS", "")
|
|
72
|
+
patterns: list[re.Pattern[str]] = []
|
|
73
|
+
for segment in raw.split(";"):
|
|
74
|
+
segment = segment.strip()
|
|
75
|
+
if not segment:
|
|
76
|
+
continue
|
|
77
|
+
try:
|
|
78
|
+
patterns.append(re.compile(segment))
|
|
79
|
+
except re.error as exc:
|
|
80
|
+
import sys
|
|
81
|
+
print(
|
|
82
|
+
f"[lore.scanner] Ignoring invalid LORE_BLOCK_PATTERNS entry "
|
|
83
|
+
f"'{segment}': {exc}",
|
|
84
|
+
file=sys.stderr,
|
|
85
|
+
)
|
|
86
|
+
return patterns
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# Compiled once at module load time.
|
|
90
|
+
_CUSTOM_BLOCK_PATTERNS: list[re.Pattern[str]] = _compile_block_patterns()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
# Public API
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def scan_content(fields: dict[str, str | Any]) -> list[dict]:
|
|
99
|
+
"""Scan concept fields for blocked patterns.
|
|
100
|
+
|
|
101
|
+
Checks each non-None field value against credential patterns, internal URL
|
|
102
|
+
patterns, and the custom blocklist. Stops at the first match per field to
|
|
103
|
+
avoid flooding the caller with redundant violations — but continues
|
|
104
|
+
checking other fields.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
fields: Mapping of field name to field value. Non-string values are
|
|
108
|
+
converted via ``str()``. ``None`` values are skipped.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
A list of violation dicts. Each entry has::
|
|
112
|
+
|
|
113
|
+
{
|
|
114
|
+
"field": str, # e.g. "content"
|
|
115
|
+
"reason": str, # human-readable description
|
|
116
|
+
"match": str, # the matched substring
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
An empty list means the content is clean.
|
|
120
|
+
"""
|
|
121
|
+
violations: list[dict] = []
|
|
122
|
+
|
|
123
|
+
for field_name, raw_value in fields.items():
|
|
124
|
+
if raw_value is None:
|
|
125
|
+
continue
|
|
126
|
+
value = raw_value if isinstance(raw_value, str) else str(raw_value)
|
|
127
|
+
|
|
128
|
+
violation = _check_field(field_name, value)
|
|
129
|
+
if violation is not None:
|
|
130
|
+
violations.append(violation)
|
|
131
|
+
|
|
132
|
+
return violations
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
# Internal helpers
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _check_field(field_name: str, value: str) -> dict | None:
|
|
141
|
+
"""Run all pattern checks against a single field value.
|
|
142
|
+
|
|
143
|
+
Returns the first violation found, or ``None`` if the field is clean.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
field_name: The logical name of the field (e.g. ``"content"``).
|
|
147
|
+
value: The string to scan.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
A violation dict or ``None``.
|
|
151
|
+
"""
|
|
152
|
+
# 1. Credential label patterns
|
|
153
|
+
m = _CREDENTIAL_LABEL_RE.search(value)
|
|
154
|
+
if m:
|
|
155
|
+
return {
|
|
156
|
+
"field": field_name,
|
|
157
|
+
"reason": "credential pattern detected (api key / token / secret)",
|
|
158
|
+
"match": m.group(0)[:80],
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# 2. Internal URL patterns
|
|
162
|
+
m = _INTERNAL_HOST_RE.search(value)
|
|
163
|
+
if m:
|
|
164
|
+
return {
|
|
165
|
+
"field": field_name,
|
|
166
|
+
"reason": "internal URL pattern detected",
|
|
167
|
+
"match": m.group(0)[:80],
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# 3. Long hex strings (raw secrets)
|
|
171
|
+
m = _LONG_HEX_RE.search(value)
|
|
172
|
+
if m:
|
|
173
|
+
return {
|
|
174
|
+
"field": field_name,
|
|
175
|
+
"reason": "long hexadecimal string detected (possible raw secret)",
|
|
176
|
+
"match": m.group(0)[:80],
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# 4. Long base64 strings (raw tokens / key material)
|
|
180
|
+
m = _LONG_B64_RE.search(value)
|
|
181
|
+
if m:
|
|
182
|
+
return {
|
|
183
|
+
"field": field_name,
|
|
184
|
+
"reason": "long base64-encoded string detected (possible token or key material)",
|
|
185
|
+
"match": m.group(0)[:80],
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# 5. Custom blocklist patterns
|
|
189
|
+
for pattern in _CUSTOM_BLOCK_PATTERNS:
|
|
190
|
+
m = pattern.search(value)
|
|
191
|
+
if m:
|
|
192
|
+
return {
|
|
193
|
+
"field": field_name,
|
|
194
|
+
"reason": f"blocked by custom pattern '{pattern.pattern}'",
|
|
195
|
+
"match": m.group(0)[:80],
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def reload_custom_patterns() -> None:
|
|
202
|
+
"""Re-read ``LORE_BLOCK_PATTERNS`` and recompile the custom blocklist.
|
|
203
|
+
|
|
204
|
+
Call this in tests or after changing the environment variable mid-process.
|
|
205
|
+
Under normal server operation the patterns are compiled once at startup.
|
|
206
|
+
"""
|
|
207
|
+
global _CUSTOM_BLOCK_PATTERNS
|
|
208
|
+
_CUSTOM_BLOCK_PATTERNS = _compile_block_patterns()
|
lore/mcp/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Lore MCP server package.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# lore/mcp/backends — pluggable backend implementations for the Lore MCP server.
|