944 lines
28 KiB
Markdown
944 lines
28 KiB
Markdown
# Two MCP Server Architecture - Implementation Guide
|
|
|
|
## Overview
|
|
|
|
The projman plugin now uses **two separate MCP servers**:
|
|
1. **Gitea MCP Server** - Issues, labels, repository management
|
|
2. **Wiki.js MCP Server** - Documentation, lessons learned, knowledge base
|
|
|
|
This separation provides better maintainability, independent configuration, and leverages Wiki.js's superior documentation features.
|
|
|
|
> **⚠️ IMPORTANT:** For the definitive repository structure and configuration paths, refer to [CORRECT-ARCHITECTURE.md](./CORRECT-ARCHITECTURE.md). This guide provides detailed implementation examples and architectural deep-dive.
|
|
|
|
---
|
|
|
|
## Wiki.js Structure at Hyper Hive Labs
|
|
|
|
### Company-Wide Organization
|
|
|
|
```
|
|
Wiki.js Instance: https://wiki.hyperhivelabs.com
|
|
└── /hyper-hive-labs/ # Base path for all HHL content
|
|
├── projects/ # Project-specific documentation
|
|
│ ├── cuisineflow/
|
|
│ │ ├── lessons-learned/
|
|
│ │ │ ├── sprints/
|
|
│ │ │ │ ├── sprint-01-auth.md
|
|
│ │ │ │ ├── sprint-02-api.md
|
|
│ │ │ │ └── ...
|
|
│ │ │ ├── patterns/
|
|
│ │ │ │ ├── service-extraction.md
|
|
│ │ │ │ └── database-migration.md
|
|
│ │ │ └── INDEX.md
|
|
│ │ └── documentation/
|
|
│ │ ├── architecture/
|
|
│ │ ├── api/
|
|
│ │ └── deployment/
|
|
│ ├── cuisineflow-site/
|
|
│ │ ├── lessons-learned/
|
|
│ │ └── documentation/
|
|
│ ├── intuit-engine/
|
|
│ │ ├── lessons-learned/
|
|
│ │ └── documentation/
|
|
│ └── hhl-site/
|
|
│ ├── lessons-learned/
|
|
│ └── documentation/
|
|
├── company/ # Company-wide documentation
|
|
│ ├── processes/
|
|
│ │ ├── onboarding.md
|
|
│ │ ├── deployment.md
|
|
│ │ └── code-review.md
|
|
│ ├── standards/
|
|
│ │ ├── python-style-guide.md
|
|
│ │ ├── api-design.md
|
|
│ │ └── security.md
|
|
│ └── tools/
|
|
│ ├── gitea-guide.md
|
|
│ ├── wikijs-guide.md
|
|
│ └── claude-plugins.md
|
|
└── shared/ # Cross-project resources
|
|
├── architecture-patterns/
|
|
│ ├── microservices.md
|
|
│ ├── api-gateway.md
|
|
│ └── database-per-service.md
|
|
├── best-practices/
|
|
│ ├── error-handling.md
|
|
│ ├── logging.md
|
|
│ └── testing.md
|
|
└── tech-stack/
|
|
├── python-ecosystem.md
|
|
├── docker.md
|
|
└── ci-cd.md
|
|
```
|
|
|
|
---
|
|
|
|
## Configuration Architecture
|
|
|
|
### System-Level Configuration
|
|
|
|
**File: `~/.config/claude/gitea.env`**
|
|
```bash
|
|
GITEA_API_URL=https://gitea.hyperhivelabs.com/api/v1
|
|
GITEA_API_TOKEN=your_gitea_token_here
|
|
GITEA_OWNER=hyperhivelabs
|
|
```
|
|
|
|
**File: `~/.config/claude/wikijs.env`**
|
|
```bash
|
|
WIKIJS_API_URL=https://wiki.hyperhivelabs.com/graphql
|
|
WIKIJS_API_TOKEN=your_wikijs_token_here
|
|
WIKIJS_BASE_PATH=/hyper-hive-labs
|
|
```
|
|
|
|
**Why separate files?**
|
|
- Different services, different authentication
|
|
- Can update one without affecting the other
|
|
- Clear separation of concerns
|
|
- Easier to revoke/rotate tokens per service
|
|
|
|
### Project-Level Configuration
|
|
|
|
**File: `project-root/.env`**
|
|
```bash
|
|
# Gitea repository name
|
|
GITEA_REPO=cuisineflow
|
|
|
|
# Wiki.js project path (relative to /hyper-hive-labs)
|
|
WIKIJS_PROJECT=projects/cuisineflow
|
|
```
|
|
|
|
**Path Resolution:**
|
|
- Full Wiki.js path = `{WIKIJS_BASE_PATH}/{WIKIJS_PROJECT}`
|
|
- For cuisineflow: `/hyper-hive-labs/projects/cuisineflow`
|
|
- For intuit-engine: `/hyper-hive-labs/projects/intuit-engine`
|
|
|
|
### PMO Configuration (No Project Scope)
|
|
|
|
**PMO operates at company level:**
|
|
- **Gitea**: No `GITEA_REPO` → accesses all repos
|
|
- **Wiki.js**: No `WIKIJS_PROJECT` → accesses entire `/hyper-hive-labs` namespace
|
|
|
|
---
|
|
|
|
## Plugin Structure
|
|
|
|
### Repository Structure (CORRECT)
|
|
|
|
```
|
|
hyperhivelabs/claude-plugins/
|
|
├── mcp-servers/ # SHARED by both plugins
|
|
│ ├── gitea/
|
|
│ │ ├── .venv/
|
|
│ │ ├── requirements.txt
|
|
│ │ │ # anthropic-sdk>=0.18.0
|
|
│ │ │ # python-dotenv>=1.0.0
|
|
│ │ │ # requests>=2.31.0
|
|
│ │ │ # pydantic>=2.5.0
|
|
│ │ ├── .env.example
|
|
│ │ ├── mcp_server/
|
|
│ │ │ ├── __init__.py
|
|
│ │ │ ├── server.py
|
|
│ │ │ ├── config.py
|
|
│ │ │ ├── gitea_client.py
|
|
│ │ │ └── tools/
|
|
│ │ │ ├── issues.py
|
|
│ │ │ └── labels.py
|
|
│ │ └── tests/
|
|
│ │ ├── test_config.py
|
|
│ │ ├── test_gitea_client.py
|
|
│ │ └── test_tools.py
|
|
│ └── wikijs/
|
|
│ ├── .venv/
|
|
│ ├── requirements.txt
|
|
│ │ # anthropic-sdk>=0.18.0
|
|
│ │ # python-dotenv>=1.0.0
|
|
│ │ # gql>=3.4.0
|
|
│ │ # aiohttp>=3.9.0
|
|
│ │ # pydantic>=2.5.0
|
|
│ ├── .env.example
|
|
│ ├── mcp_server/
|
|
│ │ ├── __init__.py
|
|
│ │ ├── server.py
|
|
│ │ ├── config.py
|
|
│ │ ├── wikijs_client.py
|
|
│ │ └── tools/
|
|
│ │ ├── pages.py
|
|
│ │ ├── lessons_learned.py
|
|
│ │ └── documentation.py
|
|
│ └── tests/
|
|
│ ├── test_config.py
|
|
│ ├── test_wikijs_client.py
|
|
│ └── test_tools.py
|
|
├── projman/ # Project plugin
|
|
│ ├── .claude-plugin/
|
|
│ │ └── plugin.json
|
|
│ ├── .mcp.json # Points to ../mcp-servers/
|
|
│ ├── commands/
|
|
│ │ ├── sprint-plan.md
|
|
│ │ ├── sprint-start.md
|
|
│ │ ├── sprint-status.md
|
|
│ │ ├── sprint-close.md
|
|
│ │ └── labels-sync.md
|
|
│ ├── agents/
|
|
│ │ ├── planner.md
|
|
│ │ ├── orchestrator.md
|
|
│ │ └── executor.md
|
|
│ ├── skills/
|
|
│ │ └── label-taxonomy/
|
|
│ │ └── labels-reference.md
|
|
│ ├── README.md
|
|
│ └── CONFIGURATION.md
|
|
└── projman-pmo/ # PMO plugin
|
|
├── .claude-plugin/
|
|
│ └── plugin.json
|
|
├── .mcp.json # Points to ../mcp-servers/
|
|
├── commands/
|
|
│ ├── pmo-status.md
|
|
│ ├── pmo-priorities.md
|
|
│ ├── pmo-dependencies.md
|
|
│ └── pmo-schedule.md
|
|
├── agents/
|
|
│ └── pmo-coordinator.md
|
|
└── README.md
|
|
```
|
|
|
|
---
|
|
|
|
## MCP Configuration Files
|
|
|
|
### projman .mcp.json (Project-Scoped)
|
|
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"gitea-projman": {
|
|
"command": "python",
|
|
"args": ["-m", "mcp_server.server"],
|
|
"cwd": "${CLAUDE_PLUGIN_ROOT}/../mcp-servers/gitea",
|
|
"env": {
|
|
"PYTHONPATH": "${CLAUDE_PLUGIN_ROOT}/../mcp-servers/gitea",
|
|
"GITEA_API_URL": "${GITEA_API_URL}",
|
|
"GITEA_API_TOKEN": "${GITEA_API_TOKEN}",
|
|
"GITEA_OWNER": "${GITEA_OWNER}",
|
|
"GITEA_REPO": "${GITEA_REPO}"
|
|
}
|
|
},
|
|
"wikijs-projman": {
|
|
"command": "python",
|
|
"args": ["-m", "mcp_server.server"],
|
|
"cwd": "${CLAUDE_PLUGIN_ROOT}/../mcp-servers/wikijs",
|
|
"env": {
|
|
"PYTHONPATH": "${CLAUDE_PLUGIN_ROOT}/../mcp-servers/wikijs",
|
|
"WIKIJS_API_URL": "${WIKIJS_API_URL}",
|
|
"WIKIJS_API_TOKEN": "${WIKIJS_API_TOKEN}",
|
|
"WIKIJS_BASE_PATH": "${WIKIJS_BASE_PATH}",
|
|
"WIKIJS_PROJECT": "${WIKIJS_PROJECT}"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### projman-pmo .mcp.json (Company-Wide)
|
|
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"gitea-pmo": {
|
|
"command": "python",
|
|
"args": ["-m", "mcp_server.server"],
|
|
"cwd": "${CLAUDE_PLUGIN_ROOT}/../mcp-servers/gitea",
|
|
"env": {
|
|
"PYTHONPATH": "${CLAUDE_PLUGIN_ROOT}/../mcp-servers/gitea",
|
|
"GITEA_API_URL": "${GITEA_API_URL}",
|
|
"GITEA_API_TOKEN": "${GITEA_API_TOKEN}",
|
|
"GITEA_OWNER": "${GITEA_OWNER}"
|
|
}
|
|
},
|
|
"wikijs-pmo": {
|
|
"command": "python",
|
|
"args": ["-m", "mcp_server.server"],
|
|
"cwd": "${CLAUDE_PLUGIN_ROOT}/../mcp-servers/wikijs",
|
|
"env": {
|
|
"PYTHONPATH": "${CLAUDE_PLUGIN_ROOT}/../mcp-servers/wikijs",
|
|
"WIKIJS_API_URL": "${WIKIJS_API_URL}",
|
|
"WIKIJS_API_TOKEN": "${WIKIJS_API_TOKEN}",
|
|
"WIKIJS_BASE_PATH": "${WIKIJS_BASE_PATH}"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Critical Notes:**
|
|
- Both plugins reference `../mcp-servers/` (shared location at repository root)
|
|
- **projman**: Includes `GITEA_REPO` and `WIKIJS_PROJECT` for project-scoped operations
|
|
- **projman-pmo**: Omits project-specific variables for company-wide operations
|
|
|
|
---
|
|
|
|
## Wiki.js MCP Server Implementation
|
|
|
|
### Configuration Loader
|
|
|
|
```python
|
|
# mcp-wikijs/mcp_server/config.py
|
|
from pathlib import Path
|
|
from dotenv import load_dotenv
|
|
import os
|
|
from typing import Dict, Optional
|
|
|
|
class WikiJSConfig:
|
|
"""Hybrid configuration loader for Wiki.js"""
|
|
|
|
def __init__(self):
|
|
self.api_url: Optional[str] = None
|
|
self.api_token: Optional[str] = None
|
|
self.base_path: Optional[str] = None
|
|
self.project_path: Optional[str] = None
|
|
self.full_path: Optional[str] = None
|
|
|
|
def load(self) -> Dict[str, str]:
|
|
"""
|
|
Load Wiki.js configuration from system and project levels.
|
|
Composes full path from base_path + project_path.
|
|
"""
|
|
# Load system config
|
|
system_config = Path.home() / '.config' / 'claude' / 'wikijs.env'
|
|
if system_config.exists():
|
|
load_dotenv(system_config)
|
|
else:
|
|
raise FileNotFoundError(
|
|
f"System config not found: {system_config}\n"
|
|
"Create it with: cat > ~/.config/claude/wikijs.env"
|
|
)
|
|
|
|
# Load project config (if exists, optional for PMO)
|
|
project_config = Path.cwd() / '.env'
|
|
if project_config.exists():
|
|
load_dotenv(project_config, override=True)
|
|
|
|
# Extract values
|
|
self.api_url = os.getenv('WIKIJS_API_URL')
|
|
self.api_token = os.getenv('WIKIJS_API_TOKEN')
|
|
self.base_path = os.getenv('WIKIJS_BASE_PATH') # /hyper-hive-labs
|
|
self.project_path = os.getenv('WIKIJS_PROJECT') # projects/cuisineflow (optional)
|
|
|
|
# Compose full path
|
|
if self.project_path:
|
|
self.full_path = f"{self.base_path}/{self.project_path}"
|
|
else:
|
|
# PMO mode - entire company namespace
|
|
self.full_path = self.base_path
|
|
|
|
# Validate required variables
|
|
self._validate()
|
|
|
|
return {
|
|
'api_url': self.api_url,
|
|
'api_token': self.api_token,
|
|
'base_path': self.base_path,
|
|
'project_path': self.project_path,
|
|
'full_path': self.full_path
|
|
}
|
|
|
|
def _validate(self) -> None:
|
|
"""Validate that required configuration is present"""
|
|
required = {
|
|
'WIKIJS_API_URL': self.api_url,
|
|
'WIKIJS_API_TOKEN': self.api_token,
|
|
'WIKIJS_BASE_PATH': self.base_path
|
|
}
|
|
|
|
missing = [key for key, value in required.items() if not value]
|
|
|
|
if missing:
|
|
raise ValueError(
|
|
f"Missing required configuration: {', '.join(missing)}\n"
|
|
"Check your ~/.config/claude/wikijs.env file"
|
|
)
|
|
```
|
|
|
|
### GraphQL Client
|
|
|
|
```python
|
|
# mcp-wikijs/mcp_server/wikijs_client.py
|
|
from gql import gql, Client
|
|
from gql.transport.aiohttp import AIOHTTPTransport
|
|
from typing import List, Dict, Optional
|
|
from .config import WikiJSConfig
|
|
|
|
class WikiJSClient:
|
|
"""Client for interacting with Wiki.js GraphQL API"""
|
|
|
|
def __init__(self):
|
|
config = WikiJSConfig()
|
|
config_dict = config.load()
|
|
|
|
self.api_url = config_dict['api_url']
|
|
self.api_token = config_dict['api_token']
|
|
self.base_path = config_dict['base_path']
|
|
self.project_path = config_dict.get('project_path')
|
|
self.full_path = config_dict['full_path']
|
|
|
|
# Set up GraphQL client
|
|
transport = AIOHTTPTransport(
|
|
url=self.api_url,
|
|
headers={'Authorization': f'Bearer {self.api_token}'}
|
|
)
|
|
self.client = Client(
|
|
transport=transport,
|
|
fetch_schema_from_transport=True
|
|
)
|
|
|
|
async def search_pages(
|
|
self,
|
|
query: str,
|
|
path: Optional[str] = None,
|
|
tags: Optional[List[str]] = None
|
|
) -> List[Dict]:
|
|
"""
|
|
Search pages in Wiki.js within a specific path.
|
|
|
|
Args:
|
|
query: Search query string
|
|
path: Optional path to search within (defaults to full_path)
|
|
tags: Optional list of tags to filter by
|
|
"""
|
|
search_path = path or self.full_path
|
|
|
|
gql_query = gql("""
|
|
query SearchPages($query: String!, $path: String) {
|
|
pages {
|
|
search(query: $query, path: $path) {
|
|
results {
|
|
id
|
|
path
|
|
title
|
|
description
|
|
tags
|
|
updatedAt
|
|
}
|
|
}
|
|
}
|
|
}
|
|
""")
|
|
|
|
result = await self.client.execute(
|
|
gql_query,
|
|
variable_values={'query': query, 'path': search_path}
|
|
)
|
|
|
|
pages = result['pages']['search']['results']
|
|
|
|
# Filter by tags if specified
|
|
if tags:
|
|
pages = [
|
|
p for p in pages
|
|
if any(tag in p['tags'] for tag in tags)
|
|
]
|
|
|
|
return pages
|
|
|
|
async def get_page(self, path: str) -> Dict:
|
|
"""Fetch a specific page by path"""
|
|
gql_query = gql("""
|
|
query GetPage($path: String!) {
|
|
pages {
|
|
single(path: $path) {
|
|
id
|
|
path
|
|
title
|
|
description
|
|
content
|
|
tags
|
|
createdAt
|
|
updatedAt
|
|
}
|
|
}
|
|
}
|
|
""")
|
|
|
|
result = await self.client.execute(
|
|
gql_query,
|
|
variable_values={'path': path}
|
|
)
|
|
return result['pages']['single']
|
|
|
|
async def create_page(
|
|
self,
|
|
path: str,
|
|
title: str,
|
|
content: str,
|
|
tags: List[str],
|
|
description: str = ""
|
|
) -> Dict:
|
|
"""
|
|
Create a new page in Wiki.js.
|
|
|
|
Args:
|
|
path: Full path for the page (e.g., /hyper-hive-labs/projects/cuisineflow/lessons-learned/sprints/sprint-01)
|
|
title: Page title
|
|
content: Page content (markdown)
|
|
tags: List of tags
|
|
description: Optional description
|
|
"""
|
|
gql_mutation = gql("""
|
|
mutation CreatePage(
|
|
$path: String!,
|
|
$title: String!,
|
|
$content: String!,
|
|
$tags: [String]!,
|
|
$description: String
|
|
) {
|
|
pages {
|
|
create(
|
|
path: $path,
|
|
title: $title,
|
|
content: $content,
|
|
tags: $tags,
|
|
description: $description,
|
|
isPublished: true,
|
|
editor: "markdown"
|
|
) {
|
|
responseResult {
|
|
succeeded
|
|
errorCode
|
|
message
|
|
}
|
|
page {
|
|
id
|
|
path
|
|
title
|
|
}
|
|
}
|
|
}
|
|
}
|
|
""")
|
|
|
|
result = await self.client.execute(
|
|
gql_mutation,
|
|
variable_values={
|
|
'path': path,
|
|
'title': title,
|
|
'content': content,
|
|
'tags': tags,
|
|
'description': description
|
|
}
|
|
)
|
|
return result['pages']['create']
|
|
|
|
async def update_page(
|
|
self,
|
|
page_id: int,
|
|
content: str,
|
|
tags: Optional[List[str]] = None
|
|
) -> Dict:
|
|
"""Update existing page"""
|
|
variables = {
|
|
'id': page_id,
|
|
'content': content
|
|
}
|
|
|
|
if tags is not None:
|
|
variables['tags'] = tags
|
|
|
|
gql_mutation = gql("""
|
|
mutation UpdatePage(
|
|
$id: Int!,
|
|
$content: String!,
|
|
$tags: [String]
|
|
) {
|
|
pages {
|
|
update(
|
|
id: $id,
|
|
content: $content,
|
|
tags: $tags
|
|
) {
|
|
responseResult {
|
|
succeeded
|
|
errorCode
|
|
message
|
|
}
|
|
}
|
|
}
|
|
}
|
|
""")
|
|
|
|
result = await self.client.execute(gql_mutation, variable_values=variables)
|
|
return result['pages']['update']
|
|
|
|
async def list_pages(self, path: str) -> List[Dict]:
|
|
"""List all pages within a path"""
|
|
gql_query = gql("""
|
|
query ListPages($path: String!) {
|
|
pages {
|
|
list(path: $path, orderBy: TITLE) {
|
|
id
|
|
path
|
|
title
|
|
description
|
|
tags
|
|
updatedAt
|
|
}
|
|
}
|
|
}
|
|
""")
|
|
|
|
result = await self.client.execute(
|
|
gql_query,
|
|
variable_values={'path': path}
|
|
)
|
|
return result['pages']['list']
|
|
|
|
# Lessons Learned Specific Methods
|
|
|
|
async def create_lesson(
|
|
self,
|
|
sprint_name: str,
|
|
lesson_content: str,
|
|
tags: List[str]
|
|
) -> Dict:
|
|
"""
|
|
Create a lessons learned document for a sprint.
|
|
|
|
Args:
|
|
sprint_name: Sprint identifier (e.g., "sprint-16-intuit-engine")
|
|
lesson_content: Full lesson markdown content
|
|
tags: Tags for categorization
|
|
"""
|
|
# Compose path within project's lessons-learned/sprints/
|
|
lesson_path = f"{self.full_path}/lessons-learned/sprints/{sprint_name}"
|
|
title = f"Sprint {sprint_name.split('-')[1]}: {' '.join(sprint_name.split('-')[2:]).title()}"
|
|
|
|
return await self.create_page(
|
|
path=lesson_path,
|
|
title=title,
|
|
content=lesson_content,
|
|
tags=tags,
|
|
description=f"Lessons learned from {sprint_name}"
|
|
)
|
|
|
|
async def search_lessons(
|
|
self,
|
|
query: str,
|
|
tags: Optional[List[str]] = None
|
|
) -> List[Dict]:
|
|
"""
|
|
Search lessons learned within the current project.
|
|
|
|
Args:
|
|
query: Search keywords
|
|
tags: Optional tags to filter by
|
|
"""
|
|
lessons_path = f"{self.full_path}/lessons-learned"
|
|
return await self.search_pages(
|
|
query=query,
|
|
path=lessons_path,
|
|
tags=tags
|
|
)
|
|
|
|
# PMO Multi-Project Methods
|
|
|
|
async def search_all_projects(
|
|
self,
|
|
query: str,
|
|
tags: Optional[List[str]] = None
|
|
) -> Dict[str, List[Dict]]:
|
|
"""
|
|
Search lessons across all projects (PMO mode).
|
|
Returns results grouped by project.
|
|
"""
|
|
all_projects_path = f"{self.base_path}/projects"
|
|
results = await self.search_pages(
|
|
query=query,
|
|
path=all_projects_path,
|
|
tags=tags
|
|
)
|
|
|
|
# Group by project
|
|
by_project = {}
|
|
for result in results:
|
|
# Extract project name from path
|
|
# e.g., "/hyper-hive-labs/projects/cuisineflow/..." -> "cuisineflow"
|
|
path_parts = result['path'].split('/')
|
|
if len(path_parts) >= 4:
|
|
project = path_parts[3]
|
|
if project not in by_project:
|
|
by_project[project] = []
|
|
by_project[project].append(result)
|
|
|
|
return by_project
|
|
|
|
async def get_shared_docs(self, category: str) -> List[Dict]:
|
|
"""
|
|
Access company-wide shared documentation.
|
|
|
|
Args:
|
|
category: Category within shared/ (e.g., "architecture-patterns", "best-practices")
|
|
"""
|
|
shared_path = f"{self.base_path}/shared/{category}"
|
|
return await self.list_pages(path=shared_path)
|
|
```
|
|
|
|
---
|
|
|
|
## MCP Tools Structure
|
|
|
|
### Gitea MCP Tools
|
|
|
|
```python
|
|
# mcp-gitea/mcp_server/tools/issues.py
|
|
class IssueTools:
|
|
def __init__(self, gitea_client):
|
|
self.gitea = gitea_client
|
|
|
|
async def list_issues(self, state='open', labels=None):
|
|
"""List issues in current repository"""
|
|
return await self.gitea.list_issues(state=state, labels=labels)
|
|
|
|
async def get_issue(self, issue_number):
|
|
"""Get specific issue details"""
|
|
return await self.gitea.get_issue(issue_number)
|
|
|
|
async def create_issue(self, title, body, labels=None):
|
|
"""Create new issue"""
|
|
return await self.gitea.create_issue(title, body, labels)
|
|
|
|
# ... other issue tools
|
|
|
|
# mcp-gitea/mcp_server/tools/labels.py
|
|
class LabelTools:
|
|
def __init__(self, gitea_client):
|
|
self.gitea = gitea_client
|
|
|
|
async def get_labels(self):
|
|
"""Get all labels from repository"""
|
|
return await self.gitea.get_labels()
|
|
|
|
async def suggest_labels(self, context):
|
|
"""Suggest appropriate labels based on context"""
|
|
# Label suggestion logic using taxonomy
|
|
pass
|
|
```
|
|
|
|
### Wiki.js MCP Tools
|
|
|
|
```python
|
|
# mcp-wikijs/mcp_server/tools/pages.py
|
|
class PageTools:
|
|
def __init__(self, wikijs_client):
|
|
self.wikijs = wikijs_client
|
|
|
|
async def search_pages(self, query, path=None, tags=None):
|
|
"""Search Wiki.js pages"""
|
|
return await self.wikijs.search_pages(query, path, tags)
|
|
|
|
async def get_page(self, path):
|
|
"""Get specific page"""
|
|
return await self.wikijs.get_page(path)
|
|
|
|
async def create_page(self, path, title, content, tags):
|
|
"""Create new page"""
|
|
return await self.wikijs.create_page(path, title, content, tags)
|
|
|
|
# ... other page tools
|
|
|
|
# mcp-wikijs/mcp_server/tools/lessons_learned.py
|
|
class LessonsLearnedTools:
|
|
def __init__(self, wikijs_client):
|
|
self.wikijs = wikijs_client
|
|
|
|
async def create_lesson(self, sprint_name, content, tags):
|
|
"""Create lessons learned document"""
|
|
return await self.wikijs.create_lesson(sprint_name, content, tags)
|
|
|
|
async def search_lessons(self, query, tags=None):
|
|
"""Search past lessons"""
|
|
return await self.wikijs.search_lessons(query, tags)
|
|
|
|
async def search_all_projects(self, query, tags=None):
|
|
"""Search lessons across all projects (PMO)"""
|
|
return await self.wikijs.search_all_projects(query, tags)
|
|
```
|
|
|
|
---
|
|
|
|
## Setup Instructions
|
|
|
|
### 1. System Configuration
|
|
|
|
```bash
|
|
# Create config directory
|
|
mkdir -p ~/.config/claude
|
|
|
|
# Create Gitea config
|
|
cat > ~/.config/claude/gitea.env << EOF
|
|
GITEA_API_URL=https://gitea.hyperhivelabs.com/api/v1
|
|
GITEA_API_TOKEN=your_gitea_token
|
|
GITEA_OWNER=hyperhivelabs
|
|
EOF
|
|
|
|
# Create Wiki.js config
|
|
cat > ~/.config/claude/wikijs.env << EOF
|
|
WIKIJS_API_URL=https://wiki.hyperhivelabs.com/graphql
|
|
WIKIJS_API_TOKEN=your_wikijs_token
|
|
WIKIJS_BASE_PATH=/hyper-hive-labs
|
|
EOF
|
|
|
|
# Secure config files
|
|
chmod 600 ~/.config/claude/*.env
|
|
```
|
|
|
|
### 2. Project Configuration
|
|
|
|
```bash
|
|
# In each project root
|
|
cat > .env << EOF
|
|
GITEA_REPO=cuisineflow
|
|
WIKIJS_PROJECT=projects/cuisineflow
|
|
EOF
|
|
|
|
# Add to .gitignore
|
|
echo ".env" >> .gitignore
|
|
```
|
|
|
|
### 3. Install MCP Servers
|
|
|
|
```bash
|
|
# Gitea MCP Server
|
|
cd /path/to/claude-plugins/mcp-servers/gitea
|
|
python -m venv .venv
|
|
source .venv/bin/activate
|
|
pip install -r requirements.txt
|
|
|
|
# Wiki.js MCP Server
|
|
cd /path/to/claude-plugins/mcp-servers/wikijs
|
|
python -m venv .venv
|
|
source .venv/bin/activate
|
|
pip install -r requirements.txt
|
|
```
|
|
|
|
### 4. Initialize Wiki.js Structure
|
|
|
|
Create the base structure in Wiki.js web interface:
|
|
1. Navigate to https://wiki.hyperhivelabs.com
|
|
2. Create `/hyper-hive-labs` page
|
|
3. Create `/hyper-hive-labs/projects` page
|
|
4. Create `/hyper-hive-labs/company` page
|
|
5. Create `/hyper-hive-labs/shared` page
|
|
|
|
Or use the Wiki.js API:
|
|
```python
|
|
# One-time setup script
|
|
import asyncio
|
|
from wikijs_client import WikiJSClient
|
|
|
|
async def initialize_wiki_structure():
|
|
client = WikiJSClient()
|
|
|
|
# Create base pages
|
|
await client.create_page(
|
|
path="/hyper-hive-labs",
|
|
title="Hyper Hive Labs",
|
|
content="# Hyper Hive Labs Documentation",
|
|
tags=["company"]
|
|
)
|
|
|
|
await client.create_page(
|
|
path="/hyper-hive-labs/projects",
|
|
title="Projects",
|
|
content="# Project Documentation",
|
|
tags=["projects"]
|
|
)
|
|
|
|
# ... create other base pages
|
|
|
|
asyncio.run(initialize_wiki_structure())
|
|
```
|
|
|
|
---
|
|
|
|
## Benefits of This Architecture
|
|
|
|
### 1. Separation of Concerns
|
|
- **Gitea MCP**: Project tracking, issues, labels
|
|
- **Wiki.js MCP**: Knowledge management, documentation
|
|
|
|
### 2. Independent Configuration
|
|
- Update Gitea credentials without affecting Wiki.js
|
|
- Different token expiration policies
|
|
- Independent service availability
|
|
|
|
### 3. Better Documentation Features
|
|
- Wiki.js rich editor
|
|
- Built-in search and indexing
|
|
- Tag system
|
|
- Version history
|
|
- Access control
|
|
- Web-based review and editing
|
|
|
|
### 4. Company-Wide Knowledge Base
|
|
- Shared documentation accessible to all projects
|
|
- Cross-project lesson learning
|
|
- Best practices repository
|
|
- Onboarding materials
|
|
- Technical standards
|
|
|
|
### 5. Scalability
|
|
- Add new projects easily
|
|
- Grow company documentation organically
|
|
- PMO has visibility across everything
|
|
- Individual projects stay focused
|
|
|
|
---
|
|
|
|
## Migration from Single MCP
|
|
|
|
If you have existing Wiki content in Git:
|
|
|
|
```python
|
|
# Migration script
|
|
import asyncio
|
|
from wikijs_client import WikiJSClient
|
|
from pathlib import Path
|
|
|
|
async def migrate_lessons_to_wikijs():
|
|
"""Migrate existing lessons learned from Git to Wiki.js"""
|
|
client = WikiJSClient()
|
|
|
|
# Read existing markdown files
|
|
lessons_dir = Path("wiki/lessons-learned/sprints")
|
|
|
|
for lesson_file in lessons_dir.glob("*.md"):
|
|
content = lesson_file.read_text()
|
|
sprint_name = lesson_file.stem
|
|
|
|
# Extract tags from content (e.g., from frontmatter or hashtags)
|
|
tags = extract_tags(content)
|
|
|
|
# Create in Wiki.js
|
|
await client.create_lesson(
|
|
sprint_name=sprint_name,
|
|
lesson_content=content,
|
|
tags=tags
|
|
)
|
|
|
|
print(f"Migrated: {sprint_name}")
|
|
|
|
asyncio.run(migrate_lessons_to_wikijs())
|
|
```
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
1. **Set up Wiki.js instance** if not already done
|
|
2. **Create base structure** in Wiki.js
|
|
3. **Implement both MCP servers** (Phase 1.1a and 1.1b)
|
|
4. **Test configuration** with both services
|
|
5. **Migrate existing lessons** (if applicable)
|
|
6. **Start using with next sprint**
|
|
|
|
The two-MCP-server architecture provides a solid foundation for both project-level and company-wide knowledge management! |