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.
Files changed (62) hide show
  1. lore/__init__.py +1 -0
  2. lore/core/__init__.py +1 -0
  3. lore/core/constants.py +62 -0
  4. lore/core/scanner.py +208 -0
  5. lore/mcp/__init__.py +1 -0
  6. lore/mcp/backends/__init__.py +1 -0
  7. lore/mcp/backends/gists.py +467 -0
  8. lore/mcp/backends/gists_client.py +493 -0
  9. lore/mcp/embeddings.py +107 -0
  10. lore/mcp/models.py +100 -0
  11. lore/mcp/router.py +574 -0
  12. lore/mcp/server.py +330 -0
  13. lore/seed/__init__.py +9 -0
  14. lore/seed/concepts.py +416 -0
  15. lore/selfhosted/Dockerfile +88 -0
  16. lore/selfhosted/__init__.py +1 -0
  17. lore/selfhosted/db.py +435 -0
  18. lore/selfhosted/entrypoint.sh +15 -0
  19. lore/selfhosted/indexer.py +164 -0
  20. lore/selfhosted/schema.sql +51 -0
  21. lore/selfhosted/vector_store.py +181 -0
  22. lore/semantic_server/Dockerfile +76 -0
  23. lore/semantic_server/__init__.py +1 -0
  24. lore/semantic_server/entrypoint.sh +24 -0
  25. lore/server/__init__.py +1 -0
  26. lore/server/api.py +1012 -0
  27. lore/server/auth.py +133 -0
  28. lore/server/storage/__init__.py +5 -0
  29. lore/server/storage/base.py +139 -0
  30. lore/server/storage/gist_qdrant.py +471 -0
  31. lore/server/storage/sqlite_qdrant.py +393 -0
  32. lore/server/watcher.py +637 -0
  33. lore/tests/__init__.py +1 -0
  34. lore/tests/conftest.py +12 -0
  35. lore/tests/test_api.py +765 -0
  36. lore/tests/test_auth.py +613 -0
  37. lore/tests/test_benchmark_gists.py +348 -0
  38. lore/tests/test_benchmark_noise.py +217 -0
  39. lore/tests/test_db.py +389 -0
  40. lore/tests/test_dedup.py +710 -0
  41. lore/tests/test_embeddings.py +115 -0
  42. lore/tests/test_gists_backend.py +529 -0
  43. lore/tests/test_gists_client.py +377 -0
  44. lore/tests/test_gists_link.py +430 -0
  45. lore/tests/test_gists_rate.py +530 -0
  46. lore/tests/test_gists_search.py +563 -0
  47. lore/tests/test_indexer.py +309 -0
  48. lore/tests/test_live_registration.py +97 -0
  49. lore/tests/test_mcp_server.py +599 -0
  50. lore/tests/test_plugin_wiring.py +353 -0
  51. lore/tests/test_ratings.py +860 -0
  52. lore/tests/test_router.py +563 -0
  53. lore/tests/test_scanner.py +274 -0
  54. lore/tests/test_seed.py +261 -0
  55. lore/tests/test_semantic_api.py +737 -0
  56. lore/tests/test_storage_backend.py +506 -0
  57. lore/tests/test_vector_store.py +277 -0
  58. lore/tests/test_watcher.py +735 -0
  59. mcp_server_lore-0.1.0.dist-info/METADATA +17 -0
  60. mcp_server_lore-0.1.0.dist-info/RECORD +62 -0
  61. mcp_server_lore-0.1.0.dist-info/WHEEL +4 -0
  62. 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.