Files
leo-claude-mktplace/docs/references/MCP-WIKIJS.md
lmiranda ba599e342e refactor: update repository URL and rebrand to Bandit Labs
- Update git remote to new Tailscale hostname
- Replace old organization name (hhl-infra) with bandit
- Replace old repository name (claude-code-hhl-toolkit) with support-claude-mktplace
- Update all documentation references to use generic gitea.example.com
- Rebrand from HyperHive Labs to Bandit Labs across all files
- Rename workspace file to match new repository name

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 11:51:13 -05:00

1508 lines
42 KiB
Markdown

# 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_PROJECT` is present
- Operates within project path: `/hyper-hive-labs/projects/cuisineflow`
- Used by projman plugin
**Company Mode (pmo):**
- When `WIKIJS_PROJECT` is absent
- Operates on entire namespace: `/hyper-hive-labs`
- Used by projman-pmo plugin
```python
# 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`
```bash
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:**
1. Log into Wiki.js: https://wiki.hyperhivelabs.com
2. Navigate to: **User Menu (top-right)****Administration**
3. In the sidebar, go to: **API Access**
4. Click **Create New Token**
5. Token configuration:
- **Name:** `claude-code-mcp`
- **Expiration:** Never (or set appropriate expiration)
- **Required Permissions:**
- ✅ Read pages
- ✅ Create pages
- ✅ Update pages
- ✅ Manage tags
- ✅ Search
6. Click **Generate**
7. **Important:** Copy the token immediately (shown only once)
8. Add to configuration file (see setup below)
**Setup:**
```bash
# 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`
```bash
# Wiki.js project path (relative to base path)
WIKIJS_PROJECT=projects/cuisineflow
```
**Setup:**
```bash
# In each project root
echo "WIKIJS_PROJECT=projects/cuisineflow" >> .env
# Add to .gitignore (if not already)
echo ".env" >> .gitignore
```
### Configuration Loading Strategy
```python
# 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`
```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:**
```bash
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
```python
# 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
```python
# 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
```python
# 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
```python
# 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
```markdown
# 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
```python
# 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
```python
# 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
```bash
# 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`
```python
#!/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 Bandit 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': 'Bandit Labs',
'content': '''# Bandit Labs Documentation
Welcome to the Bandit 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': 'Bandit 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:**
```bash
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:
1. Visit https://wiki.hyperhivelabs.com/hyper-hive-labs to verify structure
2. Add additional project directories as needed
3. The structure is now ready for projman plugin to use
---
## Migration from Git-based Wiki
```python
# 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
```bash
# 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
```bash
# Solution: Verify base structure exists
# Check in Wiki.js web interface that /hyper-hive-labs path exists
```
**Issue:** Tags not working
```bash
# Solution: Ensure tags are provided as list of strings
tags = ["tag1", "tag2"] # Correct
tags = "tag1,tag2" # Wrong
```
**Issue:** PMO mode search returns nothing
```bash
# Solution: Ensure WIKIJS_PROJECT is NOT set
# Check environment variables
env | grep WIKIJS
```
---
## Security
### Best Practices
1. **Token Storage:**
- Store tokens in `~/.config/claude/wikijs.env`
- Set file permissions to 600
- Never commit tokens to git
2. **Content Validation:**
- Sanitize user input before creating pages
- Validate markdown content
- Prevent XSS in page content
3. **Access Control:**
- Use Wiki.js role-based permissions
- Limit API token permissions
- Audit access logs
---
## Next Steps
1. **Set up Wiki.js instance** (if not already done)
2. **Create base structure** using setup script
3. **Configure system and project configs**
4. **Test GraphQL connectivity**
5. **Migrate existing lessons** (if applicable)
6. **Integrate with projman plugin**