ai-skills-api/mcp/skills.py

190 lines
6.6 KiB
Python

from mcp.server.fastmcp import FastMCP
import httpx
import os
import uvicorn
MCP_INSTRUCTIONS = """You have access to a shared knowledge base (skills) and project memory via these tools:
- get_context(project): Fetch relevant skills and project conventions for the given project identifier. Always call this before answering to load relevant context.
- search_skills(query): Find skills by content/tags.
- get_memory(project): Retrieve past decisions/learnings for a project.
- add_memory(project, key, content): Store a decision or learning. ASK permission first.
- create_skill(id, name, content, ...): Create a reusable skill. ASK permission first.
CRITICAL: When you identify an opportunity to store something (a decision, pattern, convention), you MUST propose it and get explicit user confirmation before calling add_memory or create_skill. Examples:
- "I decided to use PostgreSQL. Want me to save that to memory?"
- "That's a useful pattern. I could create a reusable skill for it. Should I?"
Project conventions are scoped to a project identifier (recommended: git remote origin URL like https://github.com/username/repo). This ensures your knowledge follows you across machines. Use your shell tool to detect the git remote locally: `git remote get-url origin`.
Always pass the same project identifier consistently across sessions."""
mcp = FastMCP("skills", instructions=MCP_INSTRUCTIONS)
SKILLS_API_URL = os.getenv("SKILLS_API_URL", "http://helm:8675")
@mcp.tool()
def get_skill(skill_id: str) -> dict:
"""Get a skill by ID from the skills database"""
try:
with httpx.Client() as client:
response = client.get(f"{SKILLS_API_URL}/skills/{skill_id}")
response.raise_for_status()
return response.json()
except httpx.HTTPError as e:
return {"error": f"Failed to fetch skill: {e}"}
@mcp.tool()
def search_skills(query: str, category: str | None = None) -> list[dict]:
"""Search skills by query"""
try:
with httpx.Client() as client:
params = {"q": query}
if category:
params["category"] = category
response = client.get(f"{SKILLS_API_URL}/skills/search", params=params)
response.raise_for_status()
return response.json()
except httpx.HTTPError as e:
return [{"error": f"Failed to search skills: {e}"}]
@mcp.tool()
def list_skills(category: str | None = None) -> list[dict]:
"""List all skills, optionally filtered by category"""
try:
with httpx.Client() as client:
params = {}
if category:
params["category"] = category
response = client.get(f"{SKILLS_API_URL}/skills", params=params)
response.raise_for_status()
return response.json()
except httpx.HTTPError as e:
return [{"error": f"Failed to list skills: {e}"}]
@mcp.tool()
def get_context(project: str | None = None, skills: list[str] | None = None) -> dict:
"""Get context bundle for a project"""
try:
with httpx.Client() as client:
params = {}
if project:
params["project"] = project
if skills:
params["skills"] = ",".join(skills)
response = client.get(f"{SKILLS_API_URL}/context", params=params)
response.raise_for_status()
return response.json()
except httpx.HTTPError as e:
return {"error": f"Failed to fetch context: {e}"}
@mcp.tool()
def get_conventions(project: str | None = None) -> list[dict]:
"""Get conventions for a project"""
try:
with httpx.Client() as client:
params = {}
if project:
params["project"] = project
response = client.get(f"{SKILLS_API_URL}/conventions", params=params)
response.raise_for_status()
return response.json()
except httpx.HTTPError as e:
return [{"error": f"Failed to fetch conventions: {e}"}]
@mcp.tool()
def get_snippets(category: str | None = None, language: str | None = None) -> list[dict]:
"""Get code snippets"""
try:
with httpx.Client() as client:
params = {}
if category:
params["category"] = category
if language:
params["language"] = language
response = client.get(f"{SKILLS_API_URL}/snippets", params=params)
response.raise_for_status()
return response.json()
except httpx.HTTPError as e:
return [{"error": f"Failed to fetch snippets: {e}"}]
@mcp.tool()
def get_memory(project: str) -> list[dict]:
"""Get memory entries for a project"""
try:
with httpx.Client() as client:
params = {"project": project}
response = client.get(f"{SKILLS_API_URL}/memory", params=params)
response.raise_for_status()
return response.json()
except httpx.HTTPError as e:
return [{"error": f"Failed to fetch memory: {e}"}]
@mcp.tool()
def add_memory(project: str, key: str, content: str) -> dict:
"""Add a memory entry for a project"""
import uuid
try:
with httpx.Client() as client:
response = client.post(
f"{SKILLS_API_URL}/memory",
json={
"id": str(uuid.uuid4())[:8],
"project": project,
"key": key,
"content": content
}
)
response.raise_for_status()
return response.json()
except httpx.HTTPError as e:
return {"error": f"Failed to add memory: {e}"}
@mcp.tool()
def create_skill(
id: str,
name: str,
content: str,
category: str | None = None,
description: str | None = None,
tags: list[str] | None = None
) -> dict:
"""Create a new skill"""
try:
with httpx.Client() as client:
response = client.post(
f"{SKILLS_API_URL}/skills",
json={
"id": id,
"name": name,
"content": content,
"category": category,
"description": description,
"tags": tags
}
)
response.raise_for_status()
return response.json()
except httpx.HTTPError as e:
return {"error": f"Failed to create skill: {e}"}
if __name__ == "__main__":
transport = os.getenv("MCP_TRANSPORT", "stdio")
if transport == "sse":
host = os.getenv("MCP_HOST", "0.0.0.0")
port = int(os.getenv("MCP_PORT", "3000"))
app = mcp.sse_app()
uvicorn.run(app, host=host, port=port)
else:
mcp.run()