Added comprehensive implementation guidance to reference docs: - MCP-GITEA.md: Token generation steps, Python requirements, server implementation, async wrappers, branch detection - MCP-WIKIJS.md: Token generation steps, Wiki.js structure setup script, initialization guide - PLUGIN-PMO.md & PLUGIN-PROJMAN.md: Updated plugin manifests and configuration details - PROJECT-SUMMARY.md: Consolidated project status and architecture decisions These updates prepare the reference materials for Phase 1 implementation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
42 KiB
Wiki.js MCP Server Reference
Overview
The Wiki.js MCP Server provides integration with Wiki.js for documentation management, lessons learned capture, and knowledge base operations. It's shared by both projman and projman-pmo plugins, detecting its operating mode based on environment variables.
Location: mcp-servers/wikijs/ (repository root)
Key Features:
- Documentation page management (CRUD)
- Lessons learned capture and search
- Tag-based organization
- GraphQL API integration
- Mode detection (project-scoped vs company-wide)
- Hybrid configuration (system + project level)
- Python 3.11+ with async/await
Architecture
Mode Detection
The MCP server operates in two modes based on environment variables:
Project Mode (projman):
- When
WIKIJS_PROJECTis present - Operates within project path:
/hyper-hive-labs/projects/cuisineflow - Used by projman plugin
Company Mode (pmo):
- When
WIKIJS_PROJECTis absent - Operates on entire namespace:
/hyper-hive-labs - Used by projman-pmo plugin
# mcp-servers/wikijs/mcp_server/config.py
def load(self):
# ... load configs ...
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}"
self.mode = 'project'
else:
self.full_path = self.base_path
self.mode = 'company'
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,
'mode': self.mode
}
Wiki.js Structure
Company-Wide Organization
Wiki.js: https://wiki.hyperhivelabs.com
└── /hyper-hive-labs/ # Base path
├── 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
Path Resolution
Project Mode (projman):
- Full path =
{WIKIJS_BASE_PATH}/{WIKIJS_PROJECT} - Example:
/hyper-hive-labs/projects/cuisineflow
Company Mode (pmo):
- Full path =
{WIKIJS_BASE_PATH} - Example:
/hyper-hive-labs
Configuration
System-Level Configuration
File: ~/.config/claude/wikijs.env
WIKIJS_API_URL=https://wiki.hyperhivelabs.com/graphql
WIKIJS_API_TOKEN=your_wikijs_token
WIKIJS_BASE_PATH=/hyper-hive-labs
Generating Wiki.js API Token:
- Log into Wiki.js: https://wiki.hyperhivelabs.com
- Navigate to: User Menu (top-right) → Administration
- In the sidebar, go to: API Access
- Click Create New Token
- Token configuration:
- Name:
claude-code-mcp - Expiration: Never (or set appropriate expiration)
- Required Permissions:
- ✅ Read pages
- ✅ Create pages
- ✅ Update pages
- ✅ Manage tags
- ✅ Search
- Name:
- Click Generate
- Important: Copy the token immediately (shown only once)
- Add to configuration file (see setup below)
Setup:
# Create config directory
mkdir -p ~/.config/claude
# Create wikijs.env
cat > ~/.config/claude/wikijs.env << EOF
WIKIJS_API_URL=https://wiki.hyperhivelabs.com/graphql
WIKIJS_API_TOKEN=your_token_here
WIKIJS_BASE_PATH=/hyper-hive-labs
EOF
# Secure the file (important!)
chmod 600 ~/.config/claude/wikijs.env
# Verify setup
cat ~/.config/claude/wikijs.env
Project-Level Configuration
File: project-root/.env
# Wiki.js project path (relative to base path)
WIKIJS_PROJECT=projects/cuisineflow
Setup:
# In each project root
echo "WIKIJS_PROJECT=projects/cuisineflow" >> .env
# Add to .gitignore (if not already)
echo ".env" >> .gitignore
Configuration Loading Strategy
# mcp-servers/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
self.mode: str = 'project'
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}"
self.mode = 'project'
else:
# PMO mode - entire company namespace
self.full_path = self.base_path
self.mode = 'company'
# 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,
'mode': self.mode
}
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"
)
Directory Structure
mcp-servers/wikijs/
├── .venv/ # Python virtual environment
├── requirements.txt # Python dependencies
├── .env.example # Configuration template
├── mcp_server/
│ ├── __init__.py
│ ├── server.py # MCP server entry point
│ ├── config.py # Configuration loader
│ ├── wikijs_client.py # GraphQL client
│ └── tools/
│ ├── __init__.py
│ ├── pages.py # Page CRUD tools
│ ├── lessons_learned.py # Lessons learned tools
│ └── documentation.py # Documentation tools
└── tests/
├── test_config.py
├── test_wikijs_client.py
└── test_tools.py
Dependencies
File: mcp-servers/wikijs/requirements.txt
mcp>=0.9.0 # MCP SDK from Anthropic
python-dotenv>=1.0.0 # Environment variable loading
gql>=3.4.0 # GraphQL client for Wiki.js
aiohttp>=3.9.0 # Async HTTP
pydantic>=2.5.0 # Data validation
pytest>=7.4.3 # Testing framework
pytest-asyncio>=0.23.0 # Async testing support
Python Version: 3.10+ required
Installation:
cd mcp-servers/wikijs
python -m venv .venv
source .venv/bin/activate # Linux/Mac
# or .venv\Scripts\activate # Windows
pip install -r requirements.txt
Wiki.js GraphQL Client
# mcp-servers/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']
self.mode = config_dict['mode']
# 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
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
)
async def update_index(
self,
sprint_name: str,
tags: List[str]
) -> Dict:
"""
Update lessons learned INDEX.md with new sprint entry.
Args:
sprint_name: Sprint identifier
tags: Tags for categorization
"""
index_path = f"{self.full_path}/lessons-learned/INDEX.md"
# Get current index
try:
index_page = await self.get_page(index_path)
current_content = index_page['content']
page_id = index_page['id']
except:
# Index doesn't exist, create it
current_content = "# Lessons Learned Index\n\n## By Sprint\n\n## By Tags\n"
create_result = await self.create_page(
path=index_path,
title="Lessons Learned Index",
content=current_content,
tags=["index", "lessons-learned"],
description="Index of all lessons learned"
)
page_id = create_result['page']['id']
# Add new entry
sprint_link = f"- [Sprint {sprint_name}](sprints/{sprint_name}) {' '.join(['#' + tag for tag in tags])}\n"
# Insert after "## By Sprint" header
lines = current_content.split('\n')
insert_index = None
for i, line in enumerate(lines):
if line.startswith('## By Sprint'):
insert_index = i + 1
break
if insert_index:
lines.insert(insert_index, sprint_link)
new_content = '\n'.join(lines)
else:
new_content = current_content + "\n" + sprint_link
# Update index
return await self.update_page(page_id, new_content)
# 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")
"""
shared_path = f"{self.base_path}/shared/{category}"
return await self.list_pages(path=shared_path)
async def create_pattern_doc(
self,
pattern_name: str,
content: str,
tags: List[str]
) -> Dict:
"""
Create a pattern document in shared/architecture-patterns.
Args:
pattern_name: Pattern identifier (e.g., "service-extraction")
content: Pattern documentation (markdown)
tags: Tags for categorization
"""
pattern_path = f"{self.base_path}/shared/architecture-patterns/{pattern_name}"
title = pattern_name.replace('-', ' ').title()
return await self.create_page(
path=pattern_path,
title=title,
content=content,
tags=tags,
description=f"Architecture pattern: {title}"
)
MCP Tools
Page Management Tools
# mcp-servers/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)
async def update_page(self, page_id, content, tags=None):
"""Update existing page"""
return await self.wikijs.update_page(page_id, content, tags)
async def list_pages(self, path):
"""List all pages within a path"""
return await self.wikijs.list_pages(path)
Lessons Learned Tools
# mcp-servers/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.
Args:
sprint_name: Sprint identifier
content: Lesson content (markdown)
tags: Categorization tags
"""
result = await self.wikijs.create_lesson(sprint_name, content, tags)
# Update index
await self.wikijs.update_index(sprint_name, tags)
return result
async def search_lessons(self, query, tags=None):
"""
Search past lessons within current project.
Args:
query: Search keywords
tags: Optional tag filters
"""
return await self.wikijs.search_lessons(query, tags)
async def search_all_projects(self, query, tags=None):
"""
Search lessons across all projects (PMO mode).
Args:
query: Search keywords
tags: Optional tag filters
Returns:
Dict keyed by project name
"""
if self.wikijs.mode != 'company':
raise ValueError("search_all_projects only available in company mode")
return await self.wikijs.search_all_projects(query, tags)
async def get_lessons_by_tag(self, tag):
"""Retrieve all lessons with a specific tag"""
return await self.wikijs.search_lessons(
query="*",
tags=[tag]
)
Documentation Tools
# mcp-servers/wikijs/mcp_server/tools/documentation.py
class DocumentationTools:
def __init__(self, wikijs_client):
self.wikijs = wikijs_client
async def create_architecture_doc(self, name, content, tags):
"""Create architecture documentation"""
doc_path = f"{self.wikijs.full_path}/documentation/architecture/{name}"
return await self.wikijs.create_page(
path=doc_path,
title=name.replace('-', ' ').title(),
content=content,
tags=tags
)
async def create_api_doc(self, name, content, tags):
"""Create API documentation"""
doc_path = f"{self.wikijs.full_path}/documentation/api/{name}"
return await self.wikijs.create_page(
path=doc_path,
title=name.replace('-', ' ').title(),
content=content,
tags=tags
)
async def get_shared_patterns(self):
"""Get company-wide architecture patterns"""
return await self.wikijs.get_shared_docs("architecture-patterns")
async def get_shared_best_practices(self):
"""Get company-wide best practices"""
return await self.wikijs.get_shared_docs("best-practices")
async def create_pattern(self, pattern_name, content, tags):
"""
Create shared architecture pattern (PMO mode).
Args:
pattern_name: Pattern identifier
content: Pattern documentation
tags: Categorization tags
"""
if self.wikijs.mode != 'company':
raise ValueError("create_pattern only available in company mode")
return await self.wikijs.create_pattern_doc(pattern_name, content, tags)
Lessons Learned Workflow
Lesson Template
# Sprint [Number]: [Name] - Lessons Learned
**Date:** [Date]
**Issue:** #[number]
**Sprint Duration:** [X weeks]
## Summary
Brief overview of what was accomplished and major outcomes.
## What Went Wrong
- **Issue 1:** Description and impact
- **Root Cause:** Why it happened
- **Prevention:** How to avoid in future
- **Issue 2:** ...
## What Worked Well
- **Success 1:** What worked and why
- **Replication:** How to repeat this success
- **Success 2:** ...
## Claude Code Issues
- **Loop 1:** Description of infinite loop or blocking issue
- **Trigger:** What caused it
- **Resolution:** How it was fixed
- **Prevention:** How to avoid
## Architecture Insights
- **Decision 1:** Architectural decision made
- **Rationale:** Why this approach
- **Trade-offs:** What was compromised
- **Insight 1:** Technical learning
- **Application:** Where else this applies
## Action Items
- [ ] Task 1
- [ ] Task 2
## Tags
#service-extraction #api #refactoring #claude-code-loops
Capture Flow
User: /sprint-close
Agent: Let's capture lessons learned...
What went wrong that we should avoid next time?
User: [Response]
Agent: What decisions worked really well?
User: [Response]
Agent: Were there any Claude Code issues that caused loops/blocks?
User: [Response]
Agent: Any architectural insights for similar future work?
User: [Response]
Agent: I'll create a lesson in Wiki.js:
Path: /hyper-hive-labs/projects/cuisineflow/lessons-learned/sprints/sprint-16-intuit-engine
Tags detected:
#service-extraction #api #refactoring #claude-code-loops
Creating page in Wiki.js... ✅
Updating INDEX.md... ✅
View at: https://wiki.hyperhivelabs.com/hyper-hive-labs/projects/cuisineflow/lessons-learned/sprints/sprint-16-intuit-engine
Testing
Unit Tests
# tests/test_config.py
import pytest
from pathlib import Path
from mcp_server.config import WikiJSConfig
def test_load_system_config(tmp_path, monkeypatch):
"""Test loading system-level configuration"""
config_dir = tmp_path / '.config' / 'claude'
config_dir.mkdir(parents=True)
config_file = config_dir / 'wikijs.env'
config_file.write_text(
"WIKIJS_API_URL=https://wiki.test.com/graphql\n"
"WIKIJS_API_TOKEN=test_token\n"
"WIKIJS_BASE_PATH=/test-base\n"
)
monkeypatch.setenv('HOME', str(tmp_path))
config = WikiJSConfig()
result = config.load()
assert result['api_url'] == 'https://wiki.test.com/graphql'
assert result['api_token'] == 'test_token'
assert result['base_path'] == '/test-base'
assert result['mode'] == 'company' # No project specified
def test_project_config_path_composition(tmp_path, monkeypatch):
"""Test path composition with project config"""
system_config_dir = tmp_path / '.config' / 'claude'
system_config_dir.mkdir(parents=True)
system_config = system_config_dir / 'wikijs.env'
system_config.write_text(
"WIKIJS_API_URL=https://wiki.test.com/graphql\n"
"WIKIJS_API_TOKEN=test_token\n"
"WIKIJS_BASE_PATH=/hyper-hive-labs\n"
)
project_dir = tmp_path / 'project'
project_dir.mkdir()
project_config = project_dir / '.env'
project_config.write_text("WIKIJS_PROJECT=projects/cuisineflow\n")
monkeypatch.setenv('HOME', str(tmp_path))
monkeypatch.chdir(project_dir)
config = WikiJSConfig()
result = config.load()
assert result['project_path'] == 'projects/cuisineflow'
assert result['full_path'] == '/hyper-hive-labs/projects/cuisineflow'
assert result['mode'] == 'project'
Integration Tests
# tests/test_wikijs_client.py
import pytest
import asyncio
from mcp_server.wikijs_client import WikiJSClient
@pytest.fixture
def wikijs_client():
"""Fixture providing configured Wiki.js client"""
return WikiJSClient()
@pytest.mark.asyncio
async def test_search_pages(wikijs_client):
"""Test searching pages in Wiki.js"""
results = await wikijs_client.search_pages(query="test")
assert isinstance(results, list)
@pytest.mark.asyncio
async def test_create_lesson(wikijs_client):
"""Test creating a lessons learned document"""
lesson = await wikijs_client.create_lesson(
sprint_name="sprint-99-test",
lesson_content="# Test Lesson\n\nTest content",
tags=["test", "sprint-99"]
)
assert lesson['responseResult']['succeeded']
assert lesson['page']['title'].startswith("Sprint 99")
@pytest.mark.asyncio
async def test_search_lessons(wikijs_client):
"""Test searching lessons learned"""
results = await wikijs_client.search_lessons(
query="service extraction",
tags=["refactoring"]
)
assert isinstance(results, list)
for result in results:
assert "refactoring" in result['tags']
@pytest.mark.asyncio
async def test_pmo_search_all_projects():
"""Test PMO mode cross-project search"""
client = WikiJSClient() # Should detect company mode
if client.mode == 'company':
results = await client.search_all_projects(
query="authentication",
tags=["security"]
)
assert isinstance(results, dict)
# Results should be grouped by project
for project_name, lessons in results.items():
assert isinstance(lessons, list)
Running Tests
# Activate virtual environment
source .venv/bin/activate
# Run all tests
pytest
# Run async tests
pytest -v tests/test_wikijs_client.py
# Run with coverage
pytest --cov=mcp_server --cov-report=html
# Run specific test
pytest tests/test_config.py::test_project_config_path_composition
Wiki.js Setup
Initial Structure Creation
Status: Base structure /hyper-hive-labs does not exist and needs to be created during Phase 1.1b.
Setup Script: Run this script during Phase 1.1b to create the base structure:
File: mcp-servers/wikijs/setup_wiki_structure.py
#!/usr/bin/env python3
"""
One-time setup script to create base Wiki.js structure.
Run during Phase 1.1b implementation.
"""
import asyncio
import sys
from mcp_server.wikijs_client import WikiJSClient
async def initialize_wiki_structure():
"""Create base Wiki.js structure for Hyper Hive Labs"""
print("Initializing Wiki.js base structure...")
print("=" * 60)
try:
client = WikiJSClient()
print(f"✅ Connected to Wiki.js at {client.api_url}")
print(f" Base path: {client.base_path}")
print()
except Exception as e:
print(f"❌ Failed to connect to Wiki.js: {e}")
print(" Check your ~/.config/claude/wikijs.env configuration")
sys.exit(1)
# Base structure to create
base_pages = [
{
'path': 'hyper-hive-labs',
'title': 'Hyper Hive Labs',
'content': '''# Hyper Hive Labs Documentation
Welcome to the Hyper Hive Labs knowledge base.
## Organization
- **[Projects](hyper-hive-labs/projects)** - Project-specific documentation and lessons learned
- **[Company](hyper-hive-labs/company)** - Company-wide processes, standards, and tools
- **[Shared](hyper-hive-labs/shared)** - Cross-project architecture patterns and best practices
## Purpose
This knowledge base captures:
- Lessons learned from sprints
- Architecture patterns and decisions
- Company processes and standards
- Best practices and technical guides
All content is searchable and tagged for easy discovery across projects.
''',
'tags': ['company', 'index'],
'description': 'Hyper Hive Labs company knowledge base'
},
{
'path': 'hyper-hive-labs/projects',
'title': 'Projects',
'content': '''# Project Documentation
Project-specific documentation and lessons learned.
## Active Projects
- **[CuisineFlow](hyper-hive-labs/projects/cuisineflow)** - Main product
- **[CuisineFlow-Site](hyper-hive-labs/projects/cuisineflow-site)** - Demo and customer gateway
- **[Intuit-Engine](hyper-hive-labs/projects/intuit-engine)** - API aggregator service
- **[HHL-Site](hyper-hive-labs/projects/hhl-site)** - Company website
Each project maintains:
- Lessons learned from sprints
- Project-specific documentation
- Architecture decisions
''',
'tags': ['projects', 'index'],
'description': 'Index of all project documentation'
},
{
'path': 'hyper-hive-labs/company',
'title': 'Company',
'content': '''# Company Documentation
Company-wide processes, standards, and tools.
## Sections
- **[Processes](hyper-hive-labs/company/processes)** - Development workflows, onboarding, deployment
- **[Standards](hyper-hive-labs/company/standards)** - Code style, API design, security practices
- **[Tools](hyper-hive-labs/company/tools)** - Gitea, Wiki.js, Claude Code plugin guides
These standards apply to all projects and team members.
''',
'tags': ['company', 'processes', 'index'],
'description': 'Company processes and standards'
},
{
'path': 'hyper-hive-labs/shared',
'title': 'Shared Resources',
'content': '''# Shared Resources
Cross-project architecture patterns, best practices, and technical knowledge.
## Sections
- **[Architecture Patterns](hyper-hive-labs/shared/architecture-patterns)** - Microservices, service extraction, API design
- **[Best Practices](hyper-hive-labs/shared/best-practices)** - Error handling, logging, testing strategies
- **[Tech Stack](hyper-hive-labs/shared/tech-stack)** - Python ecosystem, Docker, CI/CD pipelines
These patterns are distilled from lessons learned across all projects.
''',
'tags': ['shared', 'resources', 'index'],
'description': 'Cross-project architecture patterns and best practices'
},
# Project placeholders
{
'path': 'hyper-hive-labs/projects/cuisineflow',
'title': 'CuisineFlow',
'content': '''# CuisineFlow
Main product - recipe management and meal planning platform.
## Documentation
- **[Lessons Learned](hyper-hive-labs/projects/cuisineflow/lessons-learned)** - Sprint retrospectives and insights
- **[Architecture](hyper-hive-labs/projects/cuisineflow/documentation/architecture)** - System architecture
- **[API](hyper-hive-labs/projects/cuisineflow/documentation/api)** - API documentation
Sprint lessons will be automatically captured here by the projman plugin.
''',
'tags': ['project', 'cuisineflow'],
'description': 'CuisineFlow project documentation'
},
{
'path': 'hyper-hive-labs/projects/cuisineflow/lessons-learned',
'title': 'CuisineFlow - Lessons Learned',
'content': '''# CuisineFlow - Lessons Learned
Sprint retrospectives and insights from CuisineFlow development.
## Organization
- **[Sprints](hyper-hive-labs/projects/cuisineflow/lessons-learned/sprints)** - Sprint-specific lessons
- **[Patterns](hyper-hive-labs/projects/cuisineflow/lessons-learned/patterns)** - Recurring patterns and solutions
- **[INDEX](hyper-hive-labs/projects/cuisineflow/lessons-learned/INDEX)** - Complete index with tags
Lessons are automatically captured during sprint close via `/sprint-close` command.
''',
'tags': ['lessons-learned', 'cuisineflow'],
'description': 'CuisineFlow lessons learned index'
}
]
# Create pages
created_count = 0
failed_count = 0
for page_data in base_pages:
try:
print(f"Creating: /{page_data['path']}")
result = await client.create_page(**page_data)
if result.get('responseResult', {}).get('succeeded'):
print(f" ✅ Created successfully")
created_count += 1
else:
error_msg = result.get('responseResult', {}).get('message', 'Unknown error')
print(f" ⚠️ Warning: {error_msg}")
failed_count += 1
except Exception as e:
print(f" ❌ Error: {e}")
failed_count += 1
print()
# Summary
print("=" * 60)
print(f"Setup complete!")
print(f" Created: {created_count} pages")
if failed_count > 0:
print(f" Failed: {failed_count} pages")
print(f"\n⚠️ Some pages failed. Check errors above.")
sys.exit(1)
else:
print(f"\n✅ All pages created successfully!")
print(f"\nView at: https://wiki.hyperhivelabs.com/hyper-hive-labs")
if __name__ == '__main__':
asyncio.run(initialize_wiki_structure())
Running the setup:
cd mcp-servers/wikijs
source .venv/bin/activate
python setup_wiki_structure.py
Expected output:
Initializing Wiki.js base structure...
============================================================
✅ Connected to Wiki.js at https://wiki.hyperhivelabs.com/graphql
Base path: /hyper-hive-labs
Creating: /hyper-hive-labs
✅ Created successfully
Creating: /hyper-hive-labs/projects
✅ Created successfully
...
============================================================
Setup complete!
Created: 6 pages
✅ All pages created successfully!
View at: https://wiki.hyperhivelabs.com/hyper-hive-labs
Post-Setup: After running the script:
- Visit https://wiki.hyperhivelabs.com/hyper-hive-labs to verify structure
- Add additional project directories as needed
- The structure is now ready for projman plugin to use
Migration from Git-based Wiki
# migrate_to_wikijs.py
import asyncio
from pathlib import Path
from mcp_server.wikijs_client import WikiJSClient
import re
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 (hashtags at end)
tags = extract_tags_from_content(content)
# Create in Wiki.js
try:
await client.create_lesson(
sprint_name=sprint_name,
lesson_content=content,
tags=tags
)
print(f"Migrated: {sprint_name}")
except Exception as e:
print(f"Error migrating {sprint_name}: {e}")
def extract_tags_from_content(content: str) -> List[str]:
"""Extract hashtags from markdown content"""
# Find all hashtags
hashtag_pattern = r'#([\w-]+)'
matches = re.findall(hashtag_pattern, content)
# Remove duplicates and return
return list(set(matches))
if __name__ == '__main__':
asyncio.run(migrate_lessons_to_wikijs())
Benefits of Wiki.js Integration
1. Superior Documentation Features
- Rich markdown editor
- Built-in search and indexing
- Tag system
- Version history
- Access control
- Web-based review and editing
- GraphQL API
2. Company-Wide Knowledge Base
- Shared documentation accessible to all projects
- Cross-project lesson learning
- Best practices repository
- Onboarding materials
- Technical standards
3. Better Collaboration
- Web interface for team review
- Comments and discussions
- Version control with history
- Role-based access
- Easy sharing with stakeholders
4. Scalability
- Add new projects easily
- Grow company documentation organically
- PMO has visibility across everything
- Individual projects stay focused
- Search across all content
Troubleshooting
Common Issues
Issue: GraphQL authentication failing
# Solution: Test token manually
curl -H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query":"{ pages { list { id title } } }"}' \
https://wiki.hyperhivelabs.com/graphql
Issue: Path not found
# Solution: Verify base structure exists
# Check in Wiki.js web interface that /hyper-hive-labs path exists
Issue: Tags not working
# Solution: Ensure tags are provided as list of strings
tags = ["tag1", "tag2"] # Correct
tags = "tag1,tag2" # Wrong
Issue: PMO mode search returns nothing
# Solution: Ensure WIKIJS_PROJECT is NOT set
# Check environment variables
env | grep WIKIJS
Security
Best Practices
-
Token Storage:
- Store tokens in
~/.config/claude/wikijs.env - Set file permissions to 600
- Never commit tokens to git
- Store tokens in
-
Content Validation:
- Sanitize user input before creating pages
- Validate markdown content
- Prevent XSS in page content
-
Access Control:
- Use Wiki.js role-based permissions
- Limit API token permissions
- Audit access logs
Next Steps
- Set up Wiki.js instance (if not already done)
- Create base structure using setup script
- Configure system and project configs
- Test GraphQL connectivity
- Migrate existing lessons (if applicable)
- Integrate with projman plugin