Commit 1929f5b8 authored by dangdoan's avatar dangdoan

update stdio

parent 25bf8018
// {
// "servers": {
// "asisstant-memory": {
// "command": "python",
// "args": ["c:\\Work\\MeU Solutions\\Project\\meu-fastmcp\\memory_mcp_server.py"],
// "type": "stdio"
// }
// }
// }
//Dùng http server thay stdio để dễ debug hơn
{ {
"servers": { "servers": {
"assistant-memory": { "assistant-memory": {
"type": "http", "command": "python",
"url": "http://mcp-dev.meucorp.com" "args": ["c:\\Work\\MeU Solutions\\Project\\meu-fastmcp\\memory_mcp_server_stdio.py"],
"type": "stdio"
} }
} }
} }
//Dùng http server thay stdio để dễ debug hơn
// {
// "servers": {
// "assistant-memory": {
// "type": "http",
// "url": "http://localhost:8090"
// }
// }
// }
\ No newline at end of file
import os import os
import pathlib import pathlib
import uuid
from datetime import datetime
import argparse
from dotenv import load_dotenv from dotenv import load_dotenv
from fastmcp import FastMCP from fastmcp import FastMCP
from qdrant_client import QdrantClient from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct, Filter, FieldCondition, MatchValue from qdrant_client.models import (
Distance,
VectorParams,
PointStruct,
Filter,
FieldCondition,
MatchValue,
)
from sentence_transformers import SentenceTransformer from sentence_transformers import SentenceTransformer
from gitlab import Gitlab from gitlab import Gitlab
import uuid
from datetime import datetime
load_dotenv() load_dotenv()
# Config # Config
QDRANT_HOST = os.getenv("QDRANT_HOST", "192.168.0.29") QDRANT_HOST = os.getenv("QDRANT_HOST", "192.168.0.29")
QDRANT_PORT = int(os.getenv("QDRANT_PORT", 7333)) QDRANT_PORT = int(os.getenv("QDRANT_PORT", "7333"))
COLLECTION_NAME = "multi_project_memory" COLLECTION_NAME = os.getenv("QDRANT_COLLECTION", "multi_project_memory")
GITLAB = Gitlab(os.getenv("GITLAB_URL"), private_token=os.getenv("GITLAB_TOKEN"))
GITLAB_URL = os.getenv("GITLAB_URL", "")
GITLAB_TOKEN = os.getenv("GITLAB_TOKEN", "")
# MCP server # MCP server
mcp = FastMCP(name="multi-project-memory") mcp = FastMCP(name="multi-project-memory")
...@@ -24,10 +37,14 @@ mcp = FastMCP(name="multi-project-memory") ...@@ -24,10 +37,14 @@ mcp = FastMCP(name="multi-project-memory")
qdrant = QdrantClient(host=QDRANT_HOST, port=QDRANT_PORT) qdrant = QdrantClient(host=QDRANT_HOST, port=QDRANT_PORT)
embedder = SentenceTransformer("all-MiniLM-L6-v2") embedder = SentenceTransformer("all-MiniLM-L6-v2")
# Tạo collection nếu chưa có gitlab_client = None
if GITLAB_URL and GITLAB_TOKEN:
gitlab_client = Gitlab(GITLAB_URL, private_token=GITLAB_TOKEN)
# Ensure collection exists
try: try:
qdrant.get_collection(COLLECTION_NAME) qdrant.get_collection(COLLECTION_NAME)
except: except Exception:
qdrant.create_collection( qdrant.create_collection(
collection_name=COLLECTION_NAME, collection_name=COLLECTION_NAME,
vectors_config=VectorParams(size=384, distance=Distance.COSINE), vectors_config=VectorParams(size=384, distance=Distance.COSINE),
...@@ -35,25 +52,26 @@ except: ...@@ -35,25 +52,26 @@ except:
@mcp.tool() @mcp.tool()
def load_agent_context(): def load_agent_context():
"""Tự động đọc file AGENT.md từ root workspace và trả về nội dung.""" """Read AGENT.md from workspace root and save to Qdrant."""
workspace_root = pathlib.Path.cwd() workspace_root = pathlib.Path.cwd()
agent_file = workspace_root / "AGENT.md" agent_file = workspace_root / "AGENT.md"
if agent_file.exists(): if agent_file.exists():
content = agent_file.read_text(encoding="utf-8") content = agent_file.read_text(encoding="utf-8")
save_context(project_id="global", key="agent_context", content=content) save_context(project_id="global", key="agent_context", content=content)
return content return content
else:
return "AGENT.md not found in workspace root. Please create it." return "AGENT.md not found in workspace root. Please create it."
@mcp.tool() @mcp.tool()
def save_context(project_id: str, key: str, content: str): def save_context(project_id: str, key: str, content: str):
"""Lưu context/code summary vào Qdrant cho một project cụ thể. Tự động update meta key list.""" """Save a context entry to Qdrant (per project). Also maintains a meta keys list."""
unique_string = f"{project_id}_{key}" unique_string = f"{project_id}_{key}"
unique_id = str(uuid.uuid5(uuid.NAMESPACE_DNS, unique_string)) unique_id = str(uuid.uuid5(uuid.NAMESPACE_DNS, unique_string))
vector = embedder.encode(content).tolist() vector = embedder.encode(content).tolist()
qdrant.upsert( qdrant.upsert(
collection_name=COLLECTION_NAME, collection_name=COLLECTION_NAME,
points=[PointStruct( points=[
PointStruct(
id=unique_id, id=unique_id,
vector=vector, vector=vector,
payload={ payload={
...@@ -61,11 +79,13 @@ def save_context(project_id: str, key: str, content: str): ...@@ -61,11 +79,13 @@ def save_context(project_id: str, key: str, content: str):
"key": key, "key": key,
"content": content, "content": content,
"updated_at": datetime.now().isoformat(), "updated_at": datetime.now().isoformat(),
"category": key.split(":")[0] if ":" in key else "unknown" "category": key.split(":")[0] if ":" in key else "unknown",
} },
)]
) )
# Meta key update ],
)
# Update meta list
meta_key = f"{project_id}_meta:context-keys-list" meta_key = f"{project_id}_meta:context-keys-list"
meta_id = str(uuid.uuid5(uuid.NAMESPACE_DNS, meta_key)) meta_id = str(uuid.uuid5(uuid.NAMESPACE_DNS, meta_key))
meta_entry = ( meta_entry = (
...@@ -74,26 +94,24 @@ def save_context(project_id: str, key: str, content: str): ...@@ -74,26 +94,24 @@ def save_context(project_id: str, key: str, content: str):
f"Updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | " f"Updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | "
f"ID: {unique_id[:8]}...\n" f"ID: {unique_id[:8]}...\n"
) )
meta_content = meta_entry meta_content = meta_entry
try: try:
retrieved = qdrant.retrieve( retrieved = qdrant.retrieve(collection_name=COLLECTION_NAME, ids=[meta_id], with_payload=True)
collection_name=COLLECTION_NAME,
ids=[meta_id],
with_payload=True
)
if retrieved and retrieved[0].payload: if retrieved and retrieved[0].payload:
old_content = retrieved[0].payload.get("content", "") old = retrieved[0].payload.get("content", "")
if f"Key: {key} " not in old_content: if f"Key: {key} " not in old:
meta_content = old_content.rstrip() + "\n" + meta_entry meta_content = old.rstrip() + "\n" + meta_entry
else: else:
meta_content = old_content meta_content = old
except Exception as e: except Exception:
print(f"Meta retrieve error: {e}")
meta_content = meta_entry meta_content = meta_entry
meta_vector = embedder.encode(meta_content).tolist() meta_vector = embedder.encode(meta_content).tolist()
qdrant.upsert( qdrant.upsert(
collection_name=COLLECTION_NAME, collection_name=COLLECTION_NAME,
points=[PointStruct( points=[
PointStruct(
id=meta_id, id=meta_id,
vector=meta_vector, vector=meta_vector,
payload={ payload={
...@@ -101,106 +119,103 @@ def save_context(project_id: str, key: str, content: str): ...@@ -101,106 +119,103 @@ def save_context(project_id: str, key: str, content: str):
"key": meta_key, "key": meta_key,
"content": meta_content, "content": meta_content,
"type": "meta_keys_list", "type": "meta_keys_list",
"updated_at": datetime.now().isoformat() "updated_at": datetime.now().isoformat(),
} },
)] )
],
) )
return f"Saved context for project '{project_id}' with key: {key}. Meta list updated." return f"Saved context for project '{project_id}' with key: {key}. Meta list updated."
@mcp.tool() @mcp.tool()
def search_context(project_id: str, query: str, limit: int = 5): def search_context(project_id: str, query: str, limit: int = 5):
"""Tìm kiếm semantic context từ Qdrant, chỉ trong project được chỉ định.""" """Semantic search within a project."""
vector = embedder.encode(query).tolist() vector = embedder.encode(query).tolist()
hits = qdrant.query_points( hits = qdrant.query_points(
collection_name=COLLECTION_NAME, collection_name=COLLECTION_NAME,
query=vector, query=vector,
query_filter=Filter( query_filter=Filter(
must=[FieldCondition( must=[
FieldCondition(
key="project_id", key="project_id",
match=MatchValue(value=str(project_id)) match=MatchValue(value=str(project_id)),
)] )
]
), ),
limit=limit limit=limit,
).points ).points
return [ return [
{ {
"key": hit.payload.get("key", hit.id), "key": hit.payload.get("key", str(hit.id)),
"content": hit.payload["content"], "content": hit.payload.get("content", ""),
"score": hit.score "score": hit.score,
} }
for hit in hits for hit in hits
if hit.payload
] ]
@mcp.tool() @mcp.tool()
def index_gitlab_repo(project_id: str, gitlab_project_id: int): def index_gitlab_repo(project_id: str, gitlab_project_id: int, ref: str = "main"):
"""Index repo GitLab vào Qdrant cho project cụ thể (lấy files và lưu summary).""" """Index files from a GitLab repo into Qdrant (first 2000 chars per file)."""
project = GITLAB.projects.get(gitlab_project_id) if gitlab_client is None:
tree = project.repository_tree(recursive=True) return "GitLab is not configured. Set GITLAB_URL and GITLAB_TOKEN."
project = gitlab_client.projects.get(gitlab_project_id)
tree = project.repository_tree(recursive=True, ref=ref)
indexed = 0 indexed = 0
for item in tree: for item in tree:
if item["type"] == "blob": if item.get("type") != "blob":
continue
path = item.get("path")
try: try:
file = project.files.get(item["path"], ref="main") f = project.files.get(path, ref=ref)
content = file.decode().decode("utf-8")[:2000] content = f.decode().decode("utf-8", errors="replace")[:2000]
save_context(project_id, item["path"], content) save_context(project_id, path, content)
indexed += 1 indexed += 1
except Exception as e: except Exception:
print(f"Error indexing {item['path']}: {e}")
continue continue
return f"Indexed {indexed} files for project '{project_id}' (GitLab ID: {gitlab_project_id})"
return f"Indexed {indexed} files for project '{project_id}' (GitLab ID: {gitlab_project_id}, ref={ref})"
@mcp.tool() @mcp.tool()
def list_all_keys(project_id: str): def list_all_keys(project_id: str):
"""Liệt kê tất cả context keys đã lưu cho project bằng cách search trực tiếp meta key.""" """List saved keys for a project from meta record."""
meta_key = f"{project_id}_meta:context-keys-list" meta_key = f"{project_id}_meta:context-keys-list"
meta_id = str(uuid.uuid5(uuid.NAMESPACE_DNS, meta_key)) meta_id = str(uuid.uuid5(uuid.NAMESPACE_DNS, meta_key))
try: try:
retrieved = qdrant.retrieve( retrieved = qdrant.retrieve(collection_name=COLLECTION_NAME, ids=[meta_id], with_payload=True)
collection_name=COLLECTION_NAME, if retrieved and retrieved[0].payload:
ids=[meta_id],
with_payload=True
)
if retrieved and len(retrieved) > 0 and retrieved[0].payload:
meta_content = retrieved[0].payload.get("content", "Danh sách trống.") meta_content = retrieved[0].payload.get("content", "Danh sách trống.")
lines = meta_content.strip().split("\n") lines = [ln.strip() for ln in meta_content.splitlines() if ln.strip()]
formatted = [f"• {line.strip()}" for line in lines if line.strip()] formatted = [f"• {ln}" for ln in lines]
total = len(formatted) return f"Danh sách context keys cho project '{project_id}' ({len(formatted)} items):\n" + "\n".join(formatted)
result = f"Danh sách context keys cho project '{project_id}' ({total} items):\n" + "\n".join(formatted) return "Không tìm thấy meta list cho project này. Hãy lưu ít nhất một context bằng save_context."
return result if formatted else "Meta list tồn tại nhưng hiện tại trống."
else:
return "Không tìm thấy meta list cho project này. Hãy lưu ít nhất một context bằng save_context, sau đó thử lại."
except Exception as e: except Exception as e:
print(f"List all keys error: {str(e)}") return f"Lỗi khi lấy meta list: {e}"
return f"Lỗi khi lấy meta list: {str(e)}. Kiểm tra log MCP server hoặc Qdrant dashboard."
@mcp.tool() @mcp.tool()
def delete_context(project_id: str, key: str): def delete_context(project_id: str, key: str):
"""Xóa một context cụ thể theo key trong project.""" """Delete one context entry by key."""
unique_string = f"{project_id}_{key}" unique_string = f"{project_id}_{key}"
unique_id = str(uuid.uuid5(uuid.NAMESPACE_DNS, unique_string)) unique_id = str(uuid.uuid5(uuid.NAMESPACE_DNS, unique_string))
from qdrant_client.models import PointIdsList from qdrant_client.models import PointIdsList
qdrant.delete(
collection_name=COLLECTION_NAME, qdrant.delete(collection_name=COLLECTION_NAME, points_selector=PointIdsList(points=[unique_id]))
points_selector=PointIdsList(points=[unique_id])
)
return f"Deleted context for project '{project_id}' with key: {key}" return f"Deleted context for project '{project_id}' with key: {key}"
@mcp.tool() @mcp.tool()
def delete_project_context(project_id: str): def delete_project_context(project_id: str):
"""Xóa toàn bộ context của một project trong Qdrant.""" """Delete all context entries of a project."""
from qdrant_client.models import FilterSelector, Filter, FieldCondition, MatchValue from qdrant_client.models import FilterSelector
qdrant.delete( qdrant.delete(
collection_name=COLLECTION_NAME, collection_name=COLLECTION_NAME,
points_selector=FilterSelector( points_selector=FilterSelector(
filter=Filter( filter=Filter(
must=[ must=[FieldCondition(key="project_id", match=MatchValue(value=str(project_id)))]
FieldCondition(
key="project_id",
match=MatchValue(value=project_id)
)
]
)
) )
),
) )
return f"Deleted all contexts for project '{project_id}'" return f"Deleted all contexts for project '{project_id}'"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment