mcp-project-context-server 0.0.7__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_project_context_server/__init__.py +6 -0
- mcp_project_context_server/__main__.py +3 -0
- mcp_project_context_server/helpers/__init__.py +0 -0
- mcp_project_context_server/helpers/context.py +32 -0
- mcp_project_context_server/indexing/__init__.py +0 -0
- mcp_project_context_server/indexing/chroma/__init__.py +0 -0
- mcp_project_context_server/indexing/chroma/indexer.py +72 -0
- mcp_project_context_server/indexing/ollama/__init__.py +0 -0
- mcp_project_context_server/indexing/ollama/embedder.py +18 -0
- mcp_project_context_server/integrations/__init__.py +0 -0
- mcp_project_context_server/integrations/chroma/__init__.py +0 -0
- mcp_project_context_server/integrations/chroma/client.py +18 -0
- mcp_project_context_server/integrations/ollama/__init__.py +0 -0
- mcp_project_context_server/integrations/ollama/client.py +28 -0
- mcp_project_context_server/server.py +119 -0
- mcp_project_context_server/tools/__init__.py +0 -0
- mcp_project_context_server/tools/index_context.py +13 -0
- mcp_project_context_server/tools/load_context.py +43 -0
- mcp_project_context_server/tools/save_session.py +43 -0
- mcp_project_context_server/tools/search_context.py +52 -0
- mcp_project_context_server-0.0.7.dist-info/METADATA +1318 -0
- mcp_project_context_server-0.0.7.dist-info/RECORD +26 -0
- mcp_project_context_server-0.0.7.dist-info/WHEEL +5 -0
- mcp_project_context_server-0.0.7.dist-info/entry_points.txt +2 -0
- mcp_project_context_server-0.0.7.dist-info/licenses/LICENSE +661 -0
- mcp_project_context_server-0.0.7.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Shared helpers for .context/ directory resolution and file reading."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def find_context_dir(project_path: str | Path) -> Path | None:
|
|
7
|
+
"""Walk up from project_path to find a .context/ directory."""
|
|
8
|
+
p = Path(project_path).resolve()
|
|
9
|
+
for candidate in [p, *p.parents]:
|
|
10
|
+
ctx = candidate / ".context"
|
|
11
|
+
if ctx.is_dir():
|
|
12
|
+
return ctx
|
|
13
|
+
return None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def collection_name_for(context_dir: Path) -> str:
|
|
17
|
+
"""Derive a stable ChromaDB collection name from the project root.
|
|
18
|
+
|
|
19
|
+
Always based on context_dir.parent so it is consistent regardless of
|
|
20
|
+
whether the caller passed a project root, a subdirectory, or a file path.
|
|
21
|
+
"""
|
|
22
|
+
project_name = context_dir.parent.name
|
|
23
|
+
return f"ctx_{project_name}".replace("-", "_").replace(" ", "_")[:63]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def read_context_files(context_dir: Path) -> dict[str, str]:
|
|
27
|
+
"""Read all markdown files from .context/ into a dict.
|
|
28
|
+
|
|
29
|
+
Keys use POSIX-style forward slashes (Path.as_posix()) so that ChromaDB
|
|
30
|
+
document IDs and metadata are identical on Windows and Linux.
|
|
31
|
+
"""
|
|
32
|
+
return {md_file.relative_to(context_dir).as_posix(): md_file.read_text(encoding="utf-8") for md_file in context_dir.rglob("*.md")}
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Indexes .context/ files into ChromaDB for semantic search."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from mcp_project_context_server.helpers.context import (
|
|
9
|
+
collection_name_for,
|
|
10
|
+
find_context_dir,
|
|
11
|
+
read_context_files,
|
|
12
|
+
)
|
|
13
|
+
from mcp_project_context_server.indexing.ollama.embedder import embed_chunk_async
|
|
14
|
+
from mcp_project_context_server.integrations.chroma.client import chroma_client
|
|
15
|
+
from mcp_project_context_server.integrations.ollama.client import get_async_client
|
|
16
|
+
|
|
17
|
+
_EMBED_CONCURRENCY: int = int(os.getenv("EMBED_CONCURRENCY", "4"))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def index_project_context(project_path: str | Path) -> str:
|
|
21
|
+
"""Chunk, embed concurrently, and batch-store all .context/ markdown files."""
|
|
22
|
+
context_dir = find_context_dir(project_path)
|
|
23
|
+
if not context_dir:
|
|
24
|
+
return f"No .context/ directory found at or above {project_path}"
|
|
25
|
+
|
|
26
|
+
col_name = collection_name_for(context_dir)
|
|
27
|
+
|
|
28
|
+
# Drop and recreate for a clean re-index
|
|
29
|
+
try:
|
|
30
|
+
chroma_client.delete_collection(col_name)
|
|
31
|
+
except Exception:
|
|
32
|
+
pass
|
|
33
|
+
collection = chroma_client.create_collection(col_name)
|
|
34
|
+
|
|
35
|
+
files = read_context_files(context_dir)
|
|
36
|
+
|
|
37
|
+
# Build flat list of (doc_id, chunk_text, filename, chunk_index)
|
|
38
|
+
all_chunks: list[tuple[str, str, str, int]] = []
|
|
39
|
+
for filename, file_content in files.items():
|
|
40
|
+
for i, chunk in enumerate(file_content[j : j + 1000] for j in range(0, len(file_content), 1000)):
|
|
41
|
+
if chunk.strip():
|
|
42
|
+
all_chunks.append((f"{filename}::{i}", chunk, filename, i))
|
|
43
|
+
|
|
44
|
+
if not all_chunks:
|
|
45
|
+
return f"Indexed 0 chunks from {len(files)} files into collection '{col_name}'"
|
|
46
|
+
|
|
47
|
+
# Embed all chunks concurrently, bounded by semaphore
|
|
48
|
+
async_client = get_async_client()
|
|
49
|
+
semaphore = asyncio.Semaphore(_EMBED_CONCURRENCY)
|
|
50
|
+
|
|
51
|
+
async def _embed(doc_id: str, chunk: str, filename: str, chunk_idx: int):
|
|
52
|
+
async with semaphore:
|
|
53
|
+
try:
|
|
54
|
+
embedding = await embed_chunk_async(chunk, async_client)
|
|
55
|
+
return (doc_id, chunk, embedding, filename, chunk_idx)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print(f"Warning: failed to embed {doc_id}: {e}", file=sys.stderr)
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
results = await asyncio.gather(*[_embed(*c) for c in all_chunks])
|
|
61
|
+
|
|
62
|
+
# Filter failures, then batch-add everything to ChromaDB in one call
|
|
63
|
+
valid = [r for r in results if r is not None]
|
|
64
|
+
if valid:
|
|
65
|
+
collection.add(
|
|
66
|
+
ids=[r[0] for r in valid],
|
|
67
|
+
embeddings=[r[2] for r in valid],
|
|
68
|
+
documents=[r[1] for r in valid],
|
|
69
|
+
metadatas=[{"file": r[3], "chunk": r[4]} for r in valid],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return f"Indexed {len(valid)} chunks from {len(files)} files into collection '{col_name}'"
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Embedding generation for the indexing pipeline (sync + async)."""
|
|
2
|
+
|
|
3
|
+
import ollama
|
|
4
|
+
|
|
5
|
+
from mcp_project_context_server.integrations.ollama.client import (
|
|
6
|
+
get_embedding,
|
|
7
|
+
get_embedding_async,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def embed_chunk(text: str) -> list[float]:
|
|
12
|
+
"""Generate an embedding vector for a single text chunk."""
|
|
13
|
+
return get_embedding(text)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def embed_chunk_async(text: str, client: ollama.AsyncClient) -> list[float]:
|
|
17
|
+
"""Async: Generate an embedding vector for a single text chunk."""
|
|
18
|
+
return await get_embedding_async(text, client)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""ChromaDB client configuration and singleton."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import chromadb
|
|
7
|
+
from chromadb.config import Settings
|
|
8
|
+
|
|
9
|
+
_chroma_default: Path = Path.home() / ".mcp-data" / "chroma"
|
|
10
|
+
CHROMA_DIR: Path = Path(os.getenv("CHROMA_DIR", str(_chroma_default)))
|
|
11
|
+
CHROMA_DIR.mkdir(parents=True, exist_ok=True)
|
|
12
|
+
|
|
13
|
+
# chromadb.PersistentClient requires a str — this is the only place a Path is
|
|
14
|
+
# explicitly converted to str, at the external API boundary.
|
|
15
|
+
chroma_client: chromadb.ClientAPI = chromadb.PersistentClient(
|
|
16
|
+
path=str(CHROMA_DIR),
|
|
17
|
+
settings=Settings(anonymized_telemetry=False),
|
|
18
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Ollama client configuration and raw embedding API calls (sync + async)."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import ollama
|
|
6
|
+
|
|
7
|
+
OLLAMA_BASE_URL: str = os.getenv("OLLAMA_HOST", "http://localhost:11434")
|
|
8
|
+
EMBED_MODEL: str = os.getenv("EMBED_MODEL", "nomic-embed-text")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_client() -> ollama.Client:
|
|
12
|
+
return ollama.Client(host=OLLAMA_BASE_URL)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_async_client() -> ollama.AsyncClient:
|
|
16
|
+
return ollama.AsyncClient(host=OLLAMA_BASE_URL)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_embedding(text: str) -> list[float]:
|
|
20
|
+
"""Call the Ollama embed endpoint and return the embedding vector."""
|
|
21
|
+
response = get_client().embed(model=EMBED_MODEL, input=text)
|
|
22
|
+
return list(response.embeddings[0])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def get_embedding_async(text: str, client: ollama.AsyncClient) -> list[float]:
|
|
26
|
+
"""Async: Call the Ollama embed endpoint and return the embedding vector."""
|
|
27
|
+
response = await client.embed(model=EMBED_MODEL, input=text)
|
|
28
|
+
return list(response.embeddings[0])
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""MCP server setup, tool registry, and entry point."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from mcp import types
|
|
9
|
+
from mcp.server import Server
|
|
10
|
+
from mcp.server.stdio import stdio_server
|
|
11
|
+
|
|
12
|
+
from mcp_project_context_server.tools import (
|
|
13
|
+
index_context,
|
|
14
|
+
load_context,
|
|
15
|
+
save_session,
|
|
16
|
+
search_context,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
_LOG_PATH = Path(r"C:\Users\drahk\.mcp-data\logs\project-context-server.log")
|
|
20
|
+
_LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
|
|
22
|
+
logging.basicConfig(
|
|
23
|
+
filename=_LOG_PATH,
|
|
24
|
+
filemode="a",
|
|
25
|
+
level=logging.DEBUG,
|
|
26
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
27
|
+
)
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
server = Server("project-context")
|
|
32
|
+
|
|
33
|
+
_TOOL_DEFINITIONS: list[types.Tool] = [
|
|
34
|
+
types.Tool(
|
|
35
|
+
name="load_project_context",
|
|
36
|
+
description=("Load the full project context for the given project path. " "Returns project.md, all ADRs, and the latest session summary. " "You MUST call this at the start of every session."),
|
|
37
|
+
inputSchema={
|
|
38
|
+
"type": "object",
|
|
39
|
+
"properties": {
|
|
40
|
+
"project_path": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"description": "Absolute path to the project root or any file within it.",
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"required": ["project_path"],
|
|
46
|
+
},
|
|
47
|
+
),
|
|
48
|
+
types.Tool(
|
|
49
|
+
name="search_project_context",
|
|
50
|
+
description=("Semantically search the indexed project context. " "Use this to find relevant past decisions, architecture notes, " "or code summaries related to your current task."),
|
|
51
|
+
inputSchema={
|
|
52
|
+
"type": "object",
|
|
53
|
+
"properties": {
|
|
54
|
+
"project_path": {"type": "string"},
|
|
55
|
+
"query": {"type": "string", "description": "Natural language search query"},
|
|
56
|
+
"n_results": {"type": "integer", "default": 5},
|
|
57
|
+
},
|
|
58
|
+
"required": ["project_path", "query"],
|
|
59
|
+
},
|
|
60
|
+
),
|
|
61
|
+
types.Tool(
|
|
62
|
+
name="save_session_summary",
|
|
63
|
+
description=("Save a summary of the current session to .context/sessions/YYYY-MM-DD.md. " "Call this at the end of a session with a concise summary of what was done."),
|
|
64
|
+
inputSchema={
|
|
65
|
+
"type": "object",
|
|
66
|
+
"properties": {
|
|
67
|
+
"project_path": {"type": "string"},
|
|
68
|
+
"summary": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"description": "Markdown summary: what was worked on, decisions made, next steps.",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
"required": ["project_path", "summary"],
|
|
74
|
+
},
|
|
75
|
+
),
|
|
76
|
+
types.Tool(
|
|
77
|
+
name="index_project_context",
|
|
78
|
+
description=("Re-index the .context/ directory into the vector store. " "Run this after updating project.md, adding ADRs, or refreshing BUNDLE.md."),
|
|
79
|
+
inputSchema={
|
|
80
|
+
"type": "object",
|
|
81
|
+
"properties": {"project_path": {"type": "string"}},
|
|
82
|
+
"required": ["project_path"],
|
|
83
|
+
},
|
|
84
|
+
),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
_TOOL_HANDLERS = {
|
|
88
|
+
"load_project_context": load_context.handle,
|
|
89
|
+
"search_project_context": search_context.handle,
|
|
90
|
+
"save_session_summary": save_session.handle,
|
|
91
|
+
"index_project_context": index_context.handle,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@server.list_tools()
|
|
96
|
+
async def list_tools() -> list[types.Tool]:
|
|
97
|
+
return _TOOL_DEFINITIONS
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@server.call_tool()
|
|
101
|
+
async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.TextContent]:
|
|
102
|
+
handler = _TOOL_HANDLERS.get(name)
|
|
103
|
+
if not handler:
|
|
104
|
+
return [types.TextContent(type="text", text=f"Unknown tool: {name}")]
|
|
105
|
+
return await handler(arguments)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def _main() -> None:
|
|
109
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
110
|
+
await server.run(read_stream, write_stream, server.create_initialization_options())
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def run() -> None:
|
|
114
|
+
logger.info("project-context-server starting")
|
|
115
|
+
try:
|
|
116
|
+
asyncio.run(_main())
|
|
117
|
+
except Exception:
|
|
118
|
+
logger.exception("Server crashed at top level")
|
|
119
|
+
raise
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Tool: index_project_context — re-indexes .context/ into ChromaDB."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from mcp import types
|
|
6
|
+
|
|
7
|
+
from mcp_project_context_server.indexing.chroma.indexer import index_project_context
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def handle(arguments: dict) -> list[types.TextContent]:
|
|
11
|
+
_project_path = os.getenv("PROJECT_PATH", arguments["project_path"])
|
|
12
|
+
result = await index_project_context(_project_path)
|
|
13
|
+
return [types.TextContent(type="text", text=result)]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Tool: load_project_context — loads project.md, ADRs, and last session."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from mcp import types
|
|
6
|
+
|
|
7
|
+
from mcp_project_context_server.helpers.context import find_context_dir
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def handle(arguments: dict) -> list[types.TextContent]:
|
|
11
|
+
_project_path = os.getenv("PROJECT_PATH", arguments["project_path"])
|
|
12
|
+
context_dir = find_context_dir(_project_path)
|
|
13
|
+
if not context_dir:
|
|
14
|
+
return [
|
|
15
|
+
types.TextContent(
|
|
16
|
+
type="text",
|
|
17
|
+
text=f"No .context/ directory found near {arguments['project_path']}",
|
|
18
|
+
)
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
parts: list[str] = []
|
|
22
|
+
|
|
23
|
+
project_md = context_dir / "project.md"
|
|
24
|
+
if project_md.exists():
|
|
25
|
+
parts.append(f"## project.md\n\n{project_md.read_text(encoding='utf-8')}")
|
|
26
|
+
|
|
27
|
+
decisions_dir = context_dir / "decisions"
|
|
28
|
+
if decisions_dir.exists():
|
|
29
|
+
adrs = sorted(decisions_dir.glob("*.md"))
|
|
30
|
+
if adrs:
|
|
31
|
+
parts.append("## Architecture Decisions\n")
|
|
32
|
+
for adr in adrs:
|
|
33
|
+
parts.append(f"### {adr.name}\n{adr.read_text(encoding='utf-8')}")
|
|
34
|
+
|
|
35
|
+
sessions_dir = context_dir / "sessions"
|
|
36
|
+
if sessions_dir.exists():
|
|
37
|
+
session_files = sorted(sessions_dir.glob("*.md"))
|
|
38
|
+
if session_files:
|
|
39
|
+
latest = session_files[-1]
|
|
40
|
+
parts.append(f"## Last Session ({latest.stem})\n\n{latest.read_text(encoding='utf-8')}")
|
|
41
|
+
|
|
42
|
+
result = "\n\n---\n\n".join(parts)
|
|
43
|
+
return [types.TextContent(type="text", text=result or "No context files found.")]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Tool: save_session_summary — writes a session summary to .context/sessions/."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from mcp import types
|
|
7
|
+
|
|
8
|
+
from mcp_project_context_server.helpers.context import find_context_dir
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def handle(arguments: dict) -> list[types.TextContent]:
|
|
12
|
+
summary: str = arguments["summary"]
|
|
13
|
+
|
|
14
|
+
_project_path = os.getenv("PROJECT_PATH", arguments["project_path"])
|
|
15
|
+
context_dir = find_context_dir(_project_path)
|
|
16
|
+
if not context_dir:
|
|
17
|
+
return [
|
|
18
|
+
types.TextContent(
|
|
19
|
+
type="text",
|
|
20
|
+
text=f"No .context/ directory found near {arguments['project_path']}",
|
|
21
|
+
)
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
sessions_dir = context_dir / "sessions"
|
|
25
|
+
sessions_dir.mkdir(exist_ok=True)
|
|
26
|
+
|
|
27
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
28
|
+
session_file = sessions_dir / f"{today}.md"
|
|
29
|
+
|
|
30
|
+
if session_file.exists():
|
|
31
|
+
timestamp = datetime.now().strftime("%H:%M")
|
|
32
|
+
file_content = f"{session_file.read_text(encoding='utf-8')}" f"\n\n### Session at {timestamp}\n\n{summary}"
|
|
33
|
+
else:
|
|
34
|
+
file_content = f"# Session: {today}\n\n{summary}"
|
|
35
|
+
|
|
36
|
+
session_file.write_text(file_content, encoding="utf-8")
|
|
37
|
+
# as_posix() gives a consistent forward-slash path regardless of platform.
|
|
38
|
+
return [
|
|
39
|
+
types.TextContent(
|
|
40
|
+
type="text",
|
|
41
|
+
text=f"Session summary saved to {session_file.as_posix()}",
|
|
42
|
+
)
|
|
43
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Tool: search_project_context — semantic search over indexed context."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
from mcp import types
|
|
8
|
+
|
|
9
|
+
from mcp_project_context_server.helpers.context import collection_name_for, find_context_dir
|
|
10
|
+
from mcp_project_context_server.integrations.chroma.client import chroma_client
|
|
11
|
+
from mcp_project_context_server.integrations.ollama.client import get_embedding
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def handle(arguments: dict) -> list[types.TextContent]:
|
|
15
|
+
query: str = arguments["query"]
|
|
16
|
+
n_results: int = arguments.get("n_results", 5)
|
|
17
|
+
|
|
18
|
+
_project_path = os.getenv("PROJECT_PATH", arguments["project_path"])
|
|
19
|
+
context_dir = find_context_dir(_project_path)
|
|
20
|
+
if not context_dir:
|
|
21
|
+
return [
|
|
22
|
+
types.TextContent(
|
|
23
|
+
type="text",
|
|
24
|
+
text=f"No .context/ directory found near {arguments['project_path']}",
|
|
25
|
+
)
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
col_name = collection_name_for(context_dir)
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
collection = chroma_client.get_collection(col_name)
|
|
32
|
+
except Exception:
|
|
33
|
+
return [
|
|
34
|
+
types.TextContent(
|
|
35
|
+
type="text",
|
|
36
|
+
text=f"Collection '{col_name}' not found. Run index_project_context first.",
|
|
37
|
+
)
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
query_embedding = get_embedding(query)
|
|
41
|
+
results = collection.query(
|
|
42
|
+
query_embeddings=cast(list[Sequence[float]], [query_embedding]),
|
|
43
|
+
n_results=min(n_results, collection.count()),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
docs = results["documents"]
|
|
47
|
+
metas = results["metadatas"]
|
|
48
|
+
if docs is None or metas is None or not docs[0]:
|
|
49
|
+
return [types.TextContent(type="text", text="No results found.")]
|
|
50
|
+
|
|
51
|
+
output_parts = [f"**[{meta['file']}]**\n{doc}" for doc, meta in zip(docs[0], metas[0])]
|
|
52
|
+
return [types.TextContent(type="text", text="\n\n---\n\n".join(output_parts))]
|