contextlayer-mcp-server 0.1.0__tar.gz
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.
- contextlayer_mcp_server-0.1.0/PKG-INFO +8 -0
- contextlayer_mcp_server-0.1.0/contextlayer/__init__.py +1 -0
- contextlayer_mcp_server-0.1.0/contextlayer/detection.py +186 -0
- contextlayer_mcp_server-0.1.0/contextlayer/server.py +26 -0
- contextlayer_mcp_server-0.1.0/contextlayer/wrapper.py +32 -0
- contextlayer_mcp_server-0.1.0/contextlayer_mcp_server.egg-info/PKG-INFO +8 -0
- contextlayer_mcp_server-0.1.0/contextlayer_mcp_server.egg-info/SOURCES.txt +12 -0
- contextlayer_mcp_server-0.1.0/contextlayer_mcp_server.egg-info/dependency_links.txt +1 -0
- contextlayer_mcp_server-0.1.0/contextlayer_mcp_server.egg-info/entry_points.txt +2 -0
- contextlayer_mcp_server-0.1.0/contextlayer_mcp_server.egg-info/requires.txt +1 -0
- contextlayer_mcp_server-0.1.0/contextlayer_mcp_server.egg-info/top_level.txt +1 -0
- contextlayer_mcp_server-0.1.0/pyproject.toml +13 -0
- contextlayer_mcp_server-0.1.0/setup.cfg +4 -0
- contextlayer_mcp_server-0.1.0/tests/test_detection.py +47 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Filesystem detection for ContextLayer. Pure functions, no MCP coupling."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Iterable
|
|
9
|
+
|
|
10
|
+
MAX_SCAN_DEPTH = 3
|
|
11
|
+
MAX_FILES_READ = 40
|
|
12
|
+
MAX_BYTES_PER_FILE = 8 * 1024
|
|
13
|
+
|
|
14
|
+
IGNORE_DIRS = {
|
|
15
|
+
".git", "node_modules", "venv", ".venv", "__pycache__",
|
|
16
|
+
"dist", "build", ".next", "target", "vendor", ".mypy_cache",
|
|
17
|
+
".pytest_cache", "coverage", ".idea", ".vscode",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
LANGUAGE_MARKERS = {
|
|
21
|
+
"package.json": "Node.js",
|
|
22
|
+
"tsconfig.json": "TypeScript",
|
|
23
|
+
"go.mod": "Go",
|
|
24
|
+
"requirements.txt": "Python",
|
|
25
|
+
"pyproject.toml": "Python",
|
|
26
|
+
"Cargo.toml": "Rust",
|
|
27
|
+
"pom.xml": "Java (Maven)",
|
|
28
|
+
"build.gradle": "Java/Kotlin (Gradle)",
|
|
29
|
+
"Gemfile": "Ruby",
|
|
30
|
+
"composer.json": "PHP",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
SOURCE_EXTS = {
|
|
34
|
+
".py", ".ts", ".tsx", ".js", ".jsx", ".go", ".rs",
|
|
35
|
+
".java", ".kt", ".rb", ".php",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
STYLE_MARKERS = {
|
|
39
|
+
".prettierrc": "prettier",
|
|
40
|
+
".prettierrc.json": "prettier",
|
|
41
|
+
".eslintrc": "eslint",
|
|
42
|
+
".eslintrc.json": "eslint",
|
|
43
|
+
"ruff.toml": "ruff",
|
|
44
|
+
".ruff.toml": "ruff",
|
|
45
|
+
"mypy.ini": "mypy",
|
|
46
|
+
".golangci.yml": "golangci-lint",
|
|
47
|
+
"rustfmt.toml": "rustfmt",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class ProjectContext:
|
|
53
|
+
root: str
|
|
54
|
+
languages: list[str] = field(default_factory=list)
|
|
55
|
+
frameworks: list[str] = field(default_factory=list)
|
|
56
|
+
paradigms: list[str] = field(default_factory=list)
|
|
57
|
+
key_files: list[str] = field(default_factory=list)
|
|
58
|
+
style_hints: list[str] = field(default_factory=list)
|
|
59
|
+
|
|
60
|
+
def env_line(self) -> str:
|
|
61
|
+
langs = ", ".join(self.languages) or "General Software Engineering"
|
|
62
|
+
if self.frameworks:
|
|
63
|
+
langs += f" (frameworks: {', '.join(self.frameworks)})"
|
|
64
|
+
return langs
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _walk(root: Path, max_depth: int) -> Iterable[Path]:
|
|
68
|
+
root = root.resolve()
|
|
69
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
70
|
+
current = Path(dirpath)
|
|
71
|
+
try:
|
|
72
|
+
depth = len(current.relative_to(root).parts)
|
|
73
|
+
except ValueError:
|
|
74
|
+
continue
|
|
75
|
+
dirnames[:] = [d for d in dirnames if d not in IGNORE_DIRS]
|
|
76
|
+
if depth >= max_depth:
|
|
77
|
+
dirnames[:] = []
|
|
78
|
+
for fn in filenames:
|
|
79
|
+
yield current / fn
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def detect_languages_and_frameworks(root: Path) -> tuple[list[str], list[str], list[str]]:
|
|
83
|
+
languages: set[str] = set()
|
|
84
|
+
frameworks: set[str] = set()
|
|
85
|
+
key_files: list[str] = []
|
|
86
|
+
|
|
87
|
+
for path in _walk(root, MAX_SCAN_DEPTH):
|
|
88
|
+
name = path.name
|
|
89
|
+
if name not in LANGUAGE_MARKERS:
|
|
90
|
+
continue
|
|
91
|
+
languages.add(LANGUAGE_MARKERS[name])
|
|
92
|
+
try:
|
|
93
|
+
key_files.append(str(path.relative_to(root)))
|
|
94
|
+
except ValueError:
|
|
95
|
+
key_files.append(str(path))
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
if name == "package.json":
|
|
99
|
+
pkg = json.loads(path.read_text(encoding="utf-8"))
|
|
100
|
+
deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
|
|
101
|
+
for fw in ("next", "react", "vue", "express", "nestjs", "svelte", "vitest", "jest"):
|
|
102
|
+
if any(fw in d for d in deps):
|
|
103
|
+
frameworks.add(fw)
|
|
104
|
+
elif name == "go.mod":
|
|
105
|
+
txt = path.read_text(encoding="utf-8", errors="ignore")
|
|
106
|
+
for fw, needle in (("gin", "gin-gonic"), ("gRPC", "grpc"), ("echo", "labstack/echo")):
|
|
107
|
+
if needle in txt:
|
|
108
|
+
frameworks.add(fw)
|
|
109
|
+
elif name == "pyproject.toml":
|
|
110
|
+
txt = path.read_text(encoding="utf-8", errors="ignore").lower()
|
|
111
|
+
for fw in ("fastapi", "django", "flask", "pydantic", "pytest", "sqlalchemy"):
|
|
112
|
+
if fw in txt:
|
|
113
|
+
frameworks.add(fw)
|
|
114
|
+
elif name == "Cargo.toml":
|
|
115
|
+
txt = path.read_text(encoding="utf-8", errors="ignore")
|
|
116
|
+
for fw, needle in (("tokio", "tokio"), ("axum", "axum"), ("serde", "serde")):
|
|
117
|
+
if needle in txt:
|
|
118
|
+
frameworks.add(fw)
|
|
119
|
+
except (OSError, json.JSONDecodeError):
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
return sorted(languages), sorted(frameworks), key_files[:20]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def detect_paradigms(root: Path) -> list[str]:
|
|
126
|
+
hints: set[str] = set()
|
|
127
|
+
read_count = 0
|
|
128
|
+
|
|
129
|
+
for path in _walk(root, MAX_SCAN_DEPTH):
|
|
130
|
+
if read_count >= MAX_FILES_READ:
|
|
131
|
+
break
|
|
132
|
+
if path.suffix not in SOURCE_EXTS:
|
|
133
|
+
continue
|
|
134
|
+
try:
|
|
135
|
+
snippet = path.read_text(encoding="utf-8", errors="ignore")[:MAX_BYTES_PER_FILE]
|
|
136
|
+
except OSError:
|
|
137
|
+
continue
|
|
138
|
+
read_count += 1
|
|
139
|
+
|
|
140
|
+
if path.suffix == ".py":
|
|
141
|
+
if "from typing import" in snippet or "typing import" in snippet:
|
|
142
|
+
hints.add("typed Python")
|
|
143
|
+
if "async def" in snippet:
|
|
144
|
+
hints.add("async/await")
|
|
145
|
+
if "@dataclass" in snippet or "pydantic" in snippet.lower():
|
|
146
|
+
hints.add("dataclasses/pydantic")
|
|
147
|
+
elif path.suffix in {".ts", ".tsx"}:
|
|
148
|
+
if "interface " in snippet or " type " in snippet:
|
|
149
|
+
hints.add("TypeScript strict types")
|
|
150
|
+
if "react" in snippet.lower():
|
|
151
|
+
hints.add("React")
|
|
152
|
+
elif path.suffix == ".go":
|
|
153
|
+
if "interface {" in snippet:
|
|
154
|
+
hints.add("Go interfaces")
|
|
155
|
+
hints.add("explicit error returns")
|
|
156
|
+
elif path.suffix == ".rs":
|
|
157
|
+
if "Result<" in snippet:
|
|
158
|
+
hints.add("Rust Result-based errors")
|
|
159
|
+
elif path.suffix == ".js":
|
|
160
|
+
hints.add("JavaScript")
|
|
161
|
+
|
|
162
|
+
return sorted(hints)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def detect_style_hints(root: Path) -> list[str]:
|
|
166
|
+
hints: list[str] = []
|
|
167
|
+
for marker, label in STYLE_MARKERS.items():
|
|
168
|
+
if (root / marker).exists() and label not in hints:
|
|
169
|
+
hints.append(label)
|
|
170
|
+
return hints
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def build_project_context(current_working_dir: str) -> ProjectContext:
|
|
174
|
+
root = Path(current_working_dir)
|
|
175
|
+
if not root.is_dir():
|
|
176
|
+
return ProjectContext(root=str(root))
|
|
177
|
+
|
|
178
|
+
langs, fws, key_files = detect_languages_and_frameworks(root)
|
|
179
|
+
return ProjectContext(
|
|
180
|
+
root=str(root),
|
|
181
|
+
languages=langs,
|
|
182
|
+
frameworks=fws,
|
|
183
|
+
paradigms=detect_paradigms(root),
|
|
184
|
+
key_files=key_files,
|
|
185
|
+
style_hints=detect_style_hints(root),
|
|
186
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""MCP server entry point. Run with: python -m contextlayer.server"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from mcp.server.mcpserver import MCPServer
|
|
5
|
+
|
|
6
|
+
from contextlayer.detection import build_project_context
|
|
7
|
+
from contextlayer.wrapper import build_expert_prompt
|
|
8
|
+
|
|
9
|
+
mcp = MCPServer("ContextLayer-SE")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@mcp.tool()
|
|
13
|
+
def analyze_and_wrap_prompt(user_prompt: str, current_working_dir: str) -> str:
|
|
14
|
+
"""Detect the project's tech stack, then wrap `user_prompt` in a
|
|
15
|
+
strict senior-engineer schema before the model ever sees it."""
|
|
16
|
+
ctx = build_project_context(current_working_dir)
|
|
17
|
+
return build_expert_prompt(user_prompt, ctx)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> None:
|
|
21
|
+
import anyio
|
|
22
|
+
anyio.run(mcp.run_stdio_async)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
main()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Prompt wrapper: turns a ProjectContext + user prompt into an expert prompt."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .detection import ProjectContext
|
|
5
|
+
|
|
6
|
+
SE_DIRECTIVES = """\
|
|
7
|
+
CRITICAL OUTPUT DIRECTIVES:
|
|
8
|
+
1. Do NOT explain basic concepts. Provide direct, production-grade solutions immediately.
|
|
9
|
+
2. Enforce strict type safety and error handling matching best practices for the detected stack.
|
|
10
|
+
3. Output code snippets first, followed only by highly specific edge cases or impacted file names.
|
|
11
|
+
4. Match existing codebase paradigms layout in the working directory.
|
|
12
|
+
5. Do not introduce new frameworks or dependencies unless explicitly requested."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_expert_prompt(user_prompt: str, ctx: ProjectContext) -> str:
|
|
16
|
+
paradigms = ", ".join(ctx.paradigms) or "none detected"
|
|
17
|
+
style = ", ".join(ctx.style_hints) or "none detected"
|
|
18
|
+
manifests = "\n - ".join(ctx.key_files) or "(no manifest files found)"
|
|
19
|
+
|
|
20
|
+
return f"""ROLE: Principal Systems Architect & Senior Engineer.
|
|
21
|
+
ENVIRONMENT DETECTED: {ctx.env_line()}
|
|
22
|
+
|
|
23
|
+
DETECTED PARADIGMS: {paradigms}
|
|
24
|
+
DETECTED STYLE CONFIG: {style}
|
|
25
|
+
KEY MANIFESTS:
|
|
26
|
+
- {manifests}
|
|
27
|
+
|
|
28
|
+
{SE_DIRECTIVES}
|
|
29
|
+
|
|
30
|
+
USER REQUEST:
|
|
31
|
+
{user_prompt}
|
|
32
|
+
"""
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
contextlayer/__init__.py
|
|
3
|
+
contextlayer/detection.py
|
|
4
|
+
contextlayer/server.py
|
|
5
|
+
contextlayer/wrapper.py
|
|
6
|
+
contextlayer_mcp_server.egg-info/PKG-INFO
|
|
7
|
+
contextlayer_mcp_server.egg-info/SOURCES.txt
|
|
8
|
+
contextlayer_mcp_server.egg-info/dependency_links.txt
|
|
9
|
+
contextlayer_mcp_server.egg-info/entry_points.txt
|
|
10
|
+
contextlayer_mcp_server.egg-info/requires.txt
|
|
11
|
+
contextlayer_mcp_server.egg-info/top_level.txt
|
|
12
|
+
tests/test_detection.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mcp>=1.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
contextlayer
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "contextlayer-mcp-server" # <--- CHANGED THIS
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Invisible intelligence layer for AI coding tools (MCP)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Rudra Nalawade" }
|
|
9
|
+
]
|
|
10
|
+
dependencies = ["mcp>=1.0.0"]
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
contextlayer = "contextlayer.server:main" # <--- Keep this as "contextlayer"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from contextlayer.detection import (
|
|
4
|
+
build_project_context,
|
|
5
|
+
detect_languages_and_frameworks,
|
|
6
|
+
detect_paradigms,
|
|
7
|
+
detect_style_hints,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_node_project(tmp_path: Path):
|
|
12
|
+
(tmp_path / "package.json").write_text(
|
|
13
|
+
'{"dependencies": {"react": "^18.0.0", "next": "^14.0.0"}}'
|
|
14
|
+
)
|
|
15
|
+
(tmp_path / "index.tsx").write_text(
|
|
16
|
+
"interface User { id: string }\nimport React from 'react'\n"
|
|
17
|
+
)
|
|
18
|
+
ctx = build_project_context(str(tmp_path))
|
|
19
|
+
assert "Node.js" in ctx.languages
|
|
20
|
+
assert "react" in ctx.frameworks
|
|
21
|
+
assert "next" in ctx.frameworks
|
|
22
|
+
assert "TypeScript strict types" in ctx.paradigms
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_python_project_ignores_venv(tmp_path: Path):
|
|
26
|
+
(tmp_path / "pyproject.toml").write_text("[project]\nname='x'\ndependencies=['fastapi']\n")
|
|
27
|
+
(tmp_path / "app.py").write_text("from typing import Optional\nasync def f(): pass\n")
|
|
28
|
+
venv = tmp_path / "venv" / "lib"
|
|
29
|
+
venv.mkdir(parents=True)
|
|
30
|
+
(venv / "bad.py").write_text("this should never be read")
|
|
31
|
+
ctx = build_project_context(str(tmp_path))
|
|
32
|
+
assert "Python" in ctx.languages
|
|
33
|
+
assert "fastapi" in ctx.frameworks
|
|
34
|
+
assert "async/await" in ctx.paradigms
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_nonexistent_dir_is_safe():
|
|
38
|
+
ctx = build_project_context("/definitely/does/not/exist")
|
|
39
|
+
assert ctx.languages == []
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_style_hints(tmp_path: Path):
|
|
43
|
+
(tmp_path / ".prettierrc").write_text("{}")
|
|
44
|
+
(tmp_path / "ruff.toml").write_text("")
|
|
45
|
+
hints = detect_style_hints(tmp_path)
|
|
46
|
+
assert "prettier" in hints
|
|
47
|
+
assert "ruff" in hints
|