feat: Add MCP server stubs, development docs, and Docker updates

- Add MCP server skeleton implementations for all 7 planned servers
  (llm-gateway, knowledge-base, git, issues, filesystem, code-analysis, cicd)
- Add comprehensive DEVELOPMENT.md with setup and usage instructions
- Add BACKLOG.md with detailed phase planning
- Update docker-compose.dev.yml with Redis and Celery workers
- Update CLAUDE.md with Syndarix-specific context

Addresses issues #16, #20, #21

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-30 02:13:16 +01:00
parent 2f7124959d
commit 2310c8cdfd
19 changed files with 3060 additions and 2 deletions

70
mcp-servers/README.md Normal file
View File

@@ -0,0 +1,70 @@
# Syndarix MCP Servers
Model Context Protocol (MCP) servers providing tool access for Syndarix agents.
Per [ADR-005](../docs/adrs/ADR-005-mcp-integration.md), all tools require explicit project scoping.
## Server Overview
| Server | Priority | Purpose | Phase |
|--------|----------|---------|-------|
| llm-gateway | 1 | LLM routing with failover and cost tracking | Phase 2 |
| knowledge-base | 2 | RAG with pgvector for semantic search | Phase 2 |
| git | 3 | Git operations (clone, commit, push, PR) | Phase 2 |
| issues | 4 | Issue tracker sync (Gitea, GitHub, GitLab) | Phase 2 |
| filesystem | 5 | Sandboxed file operations | Phase 5 |
| code-analysis | 6 | AST parsing, linting, type checking | Phase 5 |
| cicd | 7 | CI/CD pipeline management | Phase 5 |
## Architecture
Each MCP server is a FastMCP application that:
1. Exposes tools via Model Context Protocol
2. Requires `project_id` for all operations (explicit scoping)
3. Uses Redis for pub/sub communication with agents
4. Logs all operations to PostgreSQL for audit
```
┌─────────────────────────────────────────────────────────────┐
│ Agent Runner │
│ │ │
│ ┌─────────────┼─────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ LLM GW │ │ Git │ │ Issues │ ... (7 total) │
│ │ MCP │ │ MCP │ │ MCP │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## Running Locally
Each MCP server runs as a separate Docker container. See `docker-compose.dev.yml` for configuration.
```bash
# Start all MCP servers (Phase 2+)
docker-compose -f docker-compose.dev.yml up -d llm-gateway knowledge-base git issues
# View logs
docker-compose logs -f llm-gateway
```
## Development
Each server follows the FastMCP pattern:
```python
from fastmcp import FastMCP
mcp = FastMCP("server-name")
@mcp.tool()
def my_tool(project_id: str, ...):
"""Tool with required project scoping."""
# Validate project access
# Execute operation
# Log for audit
pass
```
See individual server READMEs for specific tool documentation.

View File

@@ -0,0 +1,23 @@
[project]
name = "syndarix-mcp-cicd"
version = "0.1.0"
description = "Syndarix CI/CD MCP Server - Pipeline management"
requires-python = ">=3.12"
dependencies = [
"fastmcp>=0.1.0",
"httpx>=0.27.0",
"redis>=5.0.0",
"pydantic>=2.0.0",
"pydantic-settings>=2.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-asyncio>=0.23.0",
"ruff>=0.8.0",
]
[tool.ruff]
target-version = "py312"
line-length = 88

View File

@@ -0,0 +1,47 @@
"""
Syndarix CI/CD MCP Server.
Provides CI/CD pipeline management with:
- Gitea Actions integration
- GitHub Actions integration
- Pipeline status monitoring
Phase 5 implementation.
"""
import os
from fastmcp import FastMCP
mcp = FastMCP(
"syndarix-cicd",
description="CI/CD pipeline management",
)
@mcp.tool()
async def get_pipeline_status(project_id: str, run_id: str | None = None) -> dict:
"""Get CI/CD pipeline status."""
return {"status": "not_implemented", "project_id": project_id}
@mcp.tool()
async def trigger_pipeline(project_id: str, workflow: str, ref: str = "main") -> dict:
"""Trigger a CI/CD pipeline."""
return {"status": "not_implemented", "project_id": project_id}
@mcp.tool()
async def list_workflows(project_id: str) -> dict:
"""List available CI/CD workflows."""
return {"status": "not_implemented", "project_id": project_id}
@mcp.tool()
async def get_logs(project_id: str, run_id: str, job: str | None = None) -> dict:
"""Get logs from a pipeline run."""
return {"status": "not_implemented", "project_id": project_id}
if __name__ == "__main__":
mcp.run()

View File

@@ -0,0 +1,23 @@
[project]
name = "syndarix-mcp-code-analysis"
version = "0.1.0"
description = "Syndarix Code Analysis MCP Server - AST parsing, linting, type checking"
requires-python = ">=3.12"
dependencies = [
"fastmcp>=0.1.0",
"tree-sitter>=0.21.0",
"redis>=5.0.0",
"pydantic>=2.0.0",
"pydantic-settings>=2.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-asyncio>=0.23.0",
"ruff>=0.8.0",
]
[tool.ruff]
target-version = "py312"
line-length = 88

View File

@@ -0,0 +1,47 @@
"""
Syndarix Code Analysis MCP Server.
Provides code analysis with:
- AST parsing via tree-sitter
- Linting integration
- Type checking
Phase 5 implementation.
"""
import os
from fastmcp import FastMCP
mcp = FastMCP(
"syndarix-code-analysis",
description="AST parsing, linting, type checking",
)
@mcp.tool()
async def parse_file(project_id: str, path: str) -> dict:
"""Parse a file and return its AST."""
return {"status": "not_implemented", "project_id": project_id}
@mcp.tool()
async def lint_file(project_id: str, path: str) -> dict:
"""Run linting on a file."""
return {"status": "not_implemented", "project_id": project_id}
@mcp.tool()
async def type_check(project_id: str, path: str | None = None) -> dict:
"""Run type checking on file(s)."""
return {"status": "not_implemented", "project_id": project_id}
@mcp.tool()
async def find_references(project_id: str, symbol: str, path: str) -> dict:
"""Find all references to a symbol."""
return {"status": "not_implemented", "project_id": project_id}
if __name__ == "__main__":
mcp.run()

View File

@@ -0,0 +1,23 @@
[project]
name = "syndarix-mcp-filesystem"
version = "0.1.0"
description = "Syndarix Filesystem MCP Server - Sandboxed file operations"
requires-python = ">=3.12"
dependencies = [
"fastmcp>=0.1.0",
"aiofiles>=24.0.0",
"redis>=5.0.0",
"pydantic>=2.0.0",
"pydantic-settings>=2.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-asyncio>=0.23.0",
"ruff>=0.8.0",
]
[tool.ruff]
target-version = "py312"
line-length = 88

View File

@@ -0,0 +1,47 @@
"""
Syndarix Filesystem MCP Server.
Provides sandboxed file operations with:
- Project-scoped file access
- Read/write/delete operations
- Directory listing
Phase 5 implementation.
"""
import os
from fastmcp import FastMCP
mcp = FastMCP(
"syndarix-filesystem",
description="Sandboxed file operations",
)
@mcp.tool()
async def read_file(project_id: str, path: str) -> dict:
"""Read a file from the project workspace."""
return {"status": "not_implemented", "project_id": project_id}
@mcp.tool()
async def write_file(project_id: str, path: str, content: str) -> dict:
"""Write content to a file in the project workspace."""
return {"status": "not_implemented", "project_id": project_id}
@mcp.tool()
async def list_directory(project_id: str, path: str = ".") -> dict:
"""List contents of a directory."""
return {"status": "not_implemented", "project_id": project_id}
@mcp.tool()
async def delete_file(project_id: str, path: str) -> dict:
"""Delete a file from the project workspace."""
return {"status": "not_implemented", "project_id": project_id}
if __name__ == "__main__":
mcp.run()

View File

@@ -0,0 +1,24 @@
[project]
name = "syndarix-mcp-git"
version = "0.1.0"
description = "Syndarix Git MCP Server - Git operations and PR management"
requires-python = ">=3.12"
dependencies = [
"fastmcp>=0.1.0",
"gitpython>=3.1.0",
"httpx>=0.27.0",
"redis>=5.0.0",
"pydantic>=2.0.0",
"pydantic-settings>=2.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-asyncio>=0.23.0",
"ruff>=0.8.0",
]
[tool.ruff]
target-version = "py312"
line-length = 88

66
mcp-servers/git/server.py Normal file
View File

@@ -0,0 +1,66 @@
"""
Syndarix Git MCP Server.
Provides Git operations with:
- Repository management (clone, pull, push)
- Branch management
- Commit operations
- PR creation via Gitea/GitHub/GitLab APIs
Per ADR-009: Git Integration.
"""
import os
from fastmcp import FastMCP
mcp = FastMCP(
"syndarix-git",
description="Git operations and PR management",
)
@mcp.tool()
async def clone_repo(project_id: str, repo_url: str, branch: str = "main") -> dict:
"""Clone a repository for a project."""
return {"status": "not_implemented", "project_id": project_id}
@mcp.tool()
async def create_branch(project_id: str, branch_name: str, from_ref: str = "main") -> dict:
"""Create a new branch."""
return {"status": "not_implemented", "project_id": project_id}
@mcp.tool()
async def commit(project_id: str, message: str, files: list[str] | None = None) -> dict:
"""Commit changes to the repository."""
return {"status": "not_implemented", "project_id": project_id}
@mcp.tool()
async def push(project_id: str, branch: str, force: bool = False) -> dict:
"""Push changes to remote."""
return {"status": "not_implemented", "project_id": project_id}
@mcp.tool()
async def create_pr(
project_id: str,
title: str,
body: str,
head: str,
base: str = "main",
) -> dict:
"""Create a pull request."""
return {"status": "not_implemented", "project_id": project_id}
@mcp.tool()
async def get_diff(project_id: str, ref1: str = "HEAD", ref2: str | None = None) -> dict:
"""Get diff between refs."""
return {"status": "not_implemented", "project_id": project_id}
if __name__ == "__main__":
mcp.run()

View File

@@ -0,0 +1,23 @@
[project]
name = "syndarix-mcp-issues"
version = "0.1.0"
description = "Syndarix Issues MCP Server - Issue tracker synchronization"
requires-python = ">=3.12"
dependencies = [
"fastmcp>=0.1.0",
"httpx>=0.27.0",
"redis>=5.0.0",
"pydantic>=2.0.0",
"pydantic-settings>=2.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-asyncio>=0.23.0",
"ruff>=0.8.0",
]
[tool.ruff]
target-version = "py312"
line-length = 88

View File

@@ -0,0 +1,76 @@
"""
Syndarix Issues MCP Server.
Provides issue tracker operations with:
- Multi-provider support (Gitea, GitHub, GitLab)
- Bi-directional sync
- LWW conflict resolution
Per ADR-011: Issue Synchronization.
"""
import os
from fastmcp import FastMCP
mcp = FastMCP(
"syndarix-issues",
description="Issue tracker synchronization",
)
@mcp.tool()
async def list_issues(
project_id: str,
state: str = "open",
labels: list[str] | None = None,
) -> dict:
"""List issues for a project."""
return {"status": "not_implemented", "project_id": project_id}
@mcp.tool()
async def get_issue(project_id: str, issue_id: str) -> dict:
"""Get a specific issue."""
return {"status": "not_implemented", "project_id": project_id}
@mcp.tool()
async def create_issue(
project_id: str,
title: str,
body: str,
labels: list[str] | None = None,
assignees: list[str] | None = None,
) -> dict:
"""Create a new issue."""
return {"status": "not_implemented", "project_id": project_id}
@mcp.tool()
async def update_issue(
project_id: str,
issue_id: str,
title: str | None = None,
body: str | None = None,
state: str | None = None,
labels: list[str] | None = None,
) -> dict:
"""Update an existing issue."""
return {"status": "not_implemented", "project_id": project_id}
@mcp.tool()
async def add_comment(project_id: str, issue_id: str, body: str) -> dict:
"""Add a comment to an issue."""
return {"status": "not_implemented", "project_id": project_id}
@mcp.tool()
async def sync_issues(project_id: str, full: bool = False) -> dict:
"""Trigger issue sync for a project."""
return {"status": "not_implemented", "project_id": project_id}
if __name__ == "__main__":
mcp.run()

View File

@@ -0,0 +1,24 @@
[project]
name = "syndarix-mcp-knowledge-base"
version = "0.1.0"
description = "Syndarix Knowledge Base MCP Server - RAG with pgvector for semantic search"
requires-python = ">=3.12"
dependencies = [
"fastmcp>=0.1.0",
"asyncpg>=0.29.0",
"pgvector>=0.3.0",
"redis>=5.0.0",
"pydantic>=2.0.0",
"pydantic-settings>=2.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-asyncio>=0.23.0",
"ruff>=0.8.0",
]
[tool.ruff]
target-version = "py312"
line-length = 88

View File

@@ -0,0 +1,162 @@
"""
Syndarix Knowledge Base MCP Server.
Provides RAG capabilities with:
- pgvector for semantic search
- Per-project collection isolation
- Hybrid search (vector + keyword)
- Chunking strategies for code, markdown, and text
Per ADR-008: Knowledge Base RAG Architecture.
"""
import os
from fastmcp import FastMCP
# Create MCP server
mcp = FastMCP(
"syndarix-knowledge-base",
description="RAG with pgvector for semantic search",
)
# Configuration
DATABASE_URL = os.getenv("DATABASE_URL")
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
@mcp.tool()
async def search_knowledge(
project_id: str,
query: str,
top_k: int = 10,
search_type: str = "hybrid",
filters: dict | None = None,
) -> dict:
"""
Search the project knowledge base.
Args:
project_id: UUID of the project (scopes to project collection)
query: Search query text
top_k: Number of results to return
search_type: Search type (semantic, keyword, hybrid)
filters: Optional filters (file_type, path_prefix, etc.)
Returns:
List of matching documents with scores
"""
# TODO: Implement pgvector search
# 1. Generate query embedding via LLM Gateway
# 2. Search project-scoped collection
# 3. Apply filters
# 4. Return results with scores
return {
"status": "not_implemented",
"project_id": project_id,
"query": query,
}
@mcp.tool()
async def ingest_document(
project_id: str,
content: str,
source_path: str,
doc_type: str = "text",
metadata: dict | None = None,
) -> dict:
"""
Ingest a document into the knowledge base.
Args:
project_id: UUID of the project
content: Document content
source_path: Original file path for reference
doc_type: Document type (code, markdown, text)
metadata: Additional metadata
Returns:
Ingestion result with chunk count
"""
# TODO: Implement document ingestion
# 1. Apply chunking strategy based on doc_type
# 2. Generate embeddings for chunks
# 3. Store in project collection
return {
"status": "not_implemented",
"project_id": project_id,
"source_path": source_path,
}
@mcp.tool()
async def ingest_repository(
project_id: str,
repo_path: str,
include_patterns: list[str] | None = None,
exclude_patterns: list[str] | None = None,
) -> dict:
"""
Ingest an entire repository into the knowledge base.
Args:
project_id: UUID of the project
repo_path: Path to the repository
include_patterns: Glob patterns to include (e.g., ["*.py", "*.md"])
exclude_patterns: Glob patterns to exclude (e.g., ["node_modules/*"])
Returns:
Ingestion summary with file and chunk counts
"""
# TODO: Implement repository ingestion
return {
"status": "not_implemented",
"project_id": project_id,
"repo_path": repo_path,
}
@mcp.tool()
async def delete_document(
project_id: str,
source_path: str,
) -> dict:
"""
Delete a document from the knowledge base.
Args:
project_id: UUID of the project
source_path: Original file path
Returns:
Deletion result
"""
# TODO: Implement document deletion
return {
"status": "not_implemented",
"project_id": project_id,
"source_path": source_path,
}
@mcp.tool()
async def get_collection_stats(project_id: str) -> dict:
"""
Get statistics for a project's knowledge base collection.
Args:
project_id: UUID of the project
Returns:
Collection statistics (document count, chunk count, etc.)
"""
# TODO: Implement collection stats
return {
"status": "not_implemented",
"project_id": project_id,
}
if __name__ == "__main__":
mcp.run()

View File

@@ -0,0 +1,23 @@
[project]
name = "syndarix-mcp-llm-gateway"
version = "0.1.0"
description = "Syndarix LLM Gateway MCP Server - Unified LLM access with failover and cost tracking"
requires-python = ">=3.12"
dependencies = [
"fastmcp>=0.1.0",
"litellm>=1.50.0",
"redis>=5.0.0",
"pydantic>=2.0.0",
"pydantic-settings>=2.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-asyncio>=0.23.0",
"ruff>=0.8.0",
]
[tool.ruff]
target-version = "py312"
line-length = 88

View File

@@ -0,0 +1,148 @@
"""
Syndarix LLM Gateway MCP Server.
Provides unified LLM access with:
- Multi-provider support (Claude, GPT, Gemini, Qwen, DeepSeek)
- Automatic failover chains
- Cost tracking via LiteLLM callbacks
- Model group routing (high-reasoning, code-generation, fast-response, cost-optimized)
Per ADR-004: LLM Provider Abstraction.
"""
import os
from fastmcp import FastMCP
# Create MCP server
mcp = FastMCP(
"syndarix-llm-gateway",
description="Unified LLM access with failover and cost tracking",
)
# Configuration
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
DATABASE_URL = os.getenv("DATABASE_URL")
@mcp.tool()
async def chat_completion(
project_id: str,
agent_id: str,
messages: list[dict],
model_group: str = "high-reasoning",
max_tokens: int = 4096,
temperature: float = 0.7,
) -> dict:
"""
Generate a chat completion using the specified model group.
Args:
project_id: UUID of the project (required for cost attribution)
agent_id: UUID of the agent instance making the request
messages: List of message dicts with 'role' and 'content'
model_group: Model routing group (high-reasoning, code-generation, fast-response, cost-optimized, self-hosted)
max_tokens: Maximum tokens to generate
temperature: Sampling temperature (0.0-2.0)
Returns:
Completion response with content and usage statistics
"""
# TODO: Implement with LiteLLM
# 1. Map model_group to primary model + fallbacks
# 2. Check project budget before making request
# 3. Make completion request with failover
# 4. Log usage via callback
# 5. Return response
return {
"status": "not_implemented",
"project_id": project_id,
"agent_id": agent_id,
"model_group": model_group,
}
@mcp.tool()
async def get_embeddings(
project_id: str,
texts: list[str],
model: str = "text-embedding-3-small",
) -> dict:
"""
Generate embeddings for the given texts.
Args:
project_id: UUID of the project (required for cost attribution)
texts: List of texts to embed
model: Embedding model to use
Returns:
List of embedding vectors
"""
# TODO: Implement with LiteLLM embeddings
return {
"status": "not_implemented",
"project_id": project_id,
"text_count": len(texts),
}
@mcp.tool()
async def get_budget_status(project_id: str) -> dict:
"""
Get current budget status for a project.
Args:
project_id: UUID of the project
Returns:
Budget status with usage, limits, and percentage
"""
# TODO: Implement budget check from Redis
return {
"status": "not_implemented",
"project_id": project_id,
}
@mcp.tool()
async def list_available_models() -> dict:
"""
List all available models and their capabilities.
Returns:
Dictionary of model groups and available models
"""
return {
"model_groups": {
"high-reasoning": {
"primary": "claude-opus-4-5",
"fallbacks": ["gpt-5.1-codex-max", "gemini-3-pro"],
"description": "Complex analysis, architecture decisions",
},
"code-generation": {
"primary": "gpt-5.1-codex-max",
"fallbacks": ["claude-opus-4-5", "deepseek-v3.2"],
"description": "Code writing and refactoring",
},
"fast-response": {
"primary": "gemini-3-flash",
"fallbacks": ["qwen3-235b", "deepseek-v3.2"],
"description": "Quick tasks, simple queries",
},
"cost-optimized": {
"primary": "qwen3-235b",
"fallbacks": ["deepseek-v3.2"],
"description": "High-volume, non-critical tasks",
},
"self-hosted": {
"primary": "deepseek-v3.2",
"fallbacks": ["qwen3-235b"],
"description": "Privacy-sensitive, air-gapped",
},
}
}
if __name__ == "__main__":
mcp.run()