2 Commits

Author SHA1 Message Date
d353c74432 chore: release v4.1.0 2026-01-26 10:36:14 -05:00
ab4d940f94 fix(projman): add mandatory CHANGELOG and versioning to sprint-close workflow
Problem: Version workflow was documented but not enforced in sprint-close.
After completing sprints, CHANGELOG wasn't updated and releases weren't created.

Solution:
- Add "Update CHANGELOG" as mandatory step 7 in sprint-close
- Add "Version Check" as step 8 with /suggest-version and release.sh
- Update orchestrator agent with CHANGELOG and version reminders
- Add V04.1.0 wiki workflow changes to CHANGELOG [Unreleased]
- Document MCP bug #160 in Known Issues

This ensures versioning workflow is part of the sprint close process,
not just documented in CLAUDE.md.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 10:35:06 -05:00
365 changed files with 11369 additions and 35331 deletions

View File

@@ -6,12 +6,12 @@
},
"metadata": {
"description": "Project management plugins with Gitea and NetBox integrations",
"version": "7.0.0"
"version": "4.1.0"
},
"plugins": [
{
"name": "projman",
"version": "3.4.0",
"version": "3.2.0",
"description": "Sprint planning and project management with Gitea integration",
"source": "./plugins/projman",
"author": {
@@ -20,14 +20,14 @@
},
"homepage": "https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/src/branch/main/plugins/projman/README.md",
"repository": "https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace.git",
"hooks": ["./hooks/hooks.json"],
"mcpServers": ["./.mcp.json"],
"category": "development",
"tags": ["sprint", "agile", "gitea", "project-management"],
"license": "MIT"
},
{
"name": "doc-guardian",
"version": "1.1.0",
"version": "1.0.0",
"description": "Automatic documentation drift detection and synchronization",
"source": "./plugins/doc-guardian",
"author": {
@@ -43,7 +43,7 @@
},
{
"name": "code-sentinel",
"version": "1.0.1",
"version": "1.0.0",
"description": "Security scanning and code refactoring tools",
"source": "./plugins/code-sentinel",
"author": {
@@ -75,8 +75,8 @@
},
{
"name": "cmdb-assistant",
"version": "1.2.0",
"description": "NetBox CMDB integration with data quality validation and machine registration",
"version": "1.0.0",
"description": "NetBox CMDB integration for infrastructure management",
"source": "./plugins/cmdb-assistant",
"author": {
"name": "Leo Miranda",
@@ -84,15 +84,15 @@
},
"homepage": "https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/src/branch/main/plugins/cmdb-assistant/README.md",
"repository": "https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace.git",
"hooks": ["./hooks/hooks.json"],
"mcpServers": ["./.mcp.json"],
"category": "infrastructure",
"tags": ["cmdb", "netbox", "dcim", "ipam", "data-quality", "validation"],
"tags": ["cmdb", "netbox", "dcim", "ipam"],
"license": "MIT"
},
{
"name": "claude-config-maintainer",
"version": "1.2.0",
"description": "CLAUDE.md and settings.local.json optimization for Claude Code projects",
"version": "1.0.0",
"description": "CLAUDE.md optimization and maintenance for Claude Code projects",
"source": "./plugins/claude-config-maintainer",
"author": {
"name": "Leo Miranda",
@@ -100,14 +100,13 @@
},
"homepage": "https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/src/branch/main/plugins/claude-config-maintainer/README.md",
"repository": "https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace.git",
"hooks": ["./hooks/hooks.json"],
"category": "development",
"tags": ["claude-md", "configuration", "optimization"],
"license": "MIT"
},
{
"name": "clarity-assist",
"version": "1.2.0",
"version": "1.0.0",
"description": "Prompt optimization and requirement clarification with ND-friendly accommodations",
"source": "./plugins/clarity-assist",
"author": {
@@ -116,14 +115,13 @@
},
"homepage": "https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/src/branch/main/plugins/clarity-assist/README.md",
"repository": "https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace.git",
"hooks": ["./hooks/hooks.json"],
"category": "productivity",
"tags": ["prompts", "requirements", "clarification", "nd-friendly"],
"license": "MIT"
},
{
"name": "git-flow",
"version": "1.2.0",
"version": "1.0.0",
"description": "Git workflow automation with intelligent commit messages and branch management",
"source": "./plugins/git-flow",
"author": {
@@ -132,14 +130,13 @@
},
"homepage": "https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/src/branch/main/plugins/git-flow/README.md",
"repository": "https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace.git",
"hooks": ["./hooks/hooks.json"],
"category": "development",
"tags": ["git", "workflow", "commits", "branching"],
"license": "MIT"
},
{
"name": "pr-review",
"version": "1.1.0",
"version": "1.0.0",
"description": "Multi-agent pull request review with confidence scoring and actionable feedback",
"source": "./plugins/pr-review",
"author": {
@@ -148,14 +145,14 @@
},
"homepage": "https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/src/branch/main/plugins/pr-review/README.md",
"repository": "https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace.git",
"hooks": ["./hooks/hooks.json"],
"mcpServers": ["./.mcp.json"],
"category": "development",
"tags": ["code-review", "pull-requests", "security", "quality"],
"license": "MIT"
},
{
"name": "data-platform",
"version": "1.3.0",
"version": "1.0.0",
"description": "Data engineering tools with pandas, PostgreSQL/PostGIS, and dbt integration",
"source": "./plugins/data-platform",
"author": {
@@ -164,42 +161,10 @@
},
"homepage": "https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/src/branch/main/plugins/data-platform/README.md",
"repository": "https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace.git",
"hooks": ["./hooks/hooks.json"],
"mcpServers": ["./.mcp.json"],
"category": "data",
"tags": ["pandas", "postgresql", "postgis", "dbt", "data-engineering", "etl"],
"license": "MIT"
},
{
"name": "viz-platform",
"version": "1.1.0",
"description": "Visualization tools with Dash Mantine Components validation, Plotly charts, and theming",
"source": "./plugins/viz-platform",
"author": {
"name": "Leo Miranda",
"email": "leobmiranda@gmail.com"
},
"homepage": "https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/src/branch/main/plugins/viz-platform/README.md",
"repository": "https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace.git",
"hooks": ["./hooks/hooks.json"],
"category": "visualization",
"tags": ["dash", "plotly", "mantine", "charts", "dashboards", "theming", "dmc"],
"license": "MIT"
},
{
"name": "contract-validator",
"version": "1.2.0",
"description": "Cross-plugin compatibility validation and Claude.md agent verification",
"source": "./plugins/contract-validator",
"author": {
"name": "Leo Miranda",
"email": "leobmiranda@gmail.com"
},
"homepage": "https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/src/branch/main/plugins/contract-validator/README.md",
"repository": "https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace.git",
"hooks": ["./hooks/hooks.json"],
"category": "development",
"tags": ["validation", "contracts", "compatibility", "agents", "interfaces", "cross-plugin"],
"license": "MIT"
}
]
}

View File

@@ -1,249 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code when working with code in this repository.
## Project Overview
**Repository:** leo-claude-mktplace
**Version:** 3.0.1
**Status:** Production Ready
A plugin marketplace for Claude Code containing:
| Plugin | Description | Version |
|--------|-------------|---------|
| `projman` | Sprint planning and project management with Gitea integration | 3.0.0 |
| `git-flow` | Git workflow automation with smart commits and branch management | 1.0.0 |
| `pr-review` | Multi-agent PR review with confidence scoring | 1.0.0 |
| `clarity-assist` | Prompt optimization with ND-friendly accommodations | 1.0.0 |
| `doc-guardian` | Automatic documentation drift detection and synchronization | 1.0.0 |
| `code-sentinel` | Security scanning and code refactoring tools | 1.0.0 |
| `claude-config-maintainer` | CLAUDE.md optimization and maintenance | 1.0.0 |
| `cmdb-assistant` | NetBox CMDB integration for infrastructure management | 1.0.0 |
| `project-hygiene` | Post-task cleanup automation via hooks | 0.1.0 |
## Quick Start
```bash
# Validate marketplace compliance
./scripts/validate-marketplace.sh
# Setup commands (in a target project with plugin installed)
/initial-setup # First time: full setup wizard
/project-init # New project: quick config
/project-sync # After repo move: sync config
# Run projman commands
/sprint-plan # Start sprint planning
/sprint-status # Check progress
/review # Pre-close code quality review
/test-check # Verify tests before close
/sprint-close # Complete sprint
```
## Repository Structure
```
leo-claude-mktplace/
├── .claude-plugin/
│ └── marketplace.json # Marketplace manifest
├── mcp-servers/ # SHARED MCP servers (v3.0.0+)
│ ├── gitea/ # Gitea MCP (issues, PRs, wiki)
│ └── netbox/ # NetBox MCP (CMDB)
├── plugins/
│ ├── projman/ # Sprint management
│ │ ├── .claude-plugin/plugin.json
│ │ ├── .mcp.json
│ │ ├── mcp-servers/gitea -> ../../../mcp-servers/gitea # SYMLINK
│ │ ├── commands/ # 12 commands (incl. setup)
│ │ ├── hooks/ # SessionStart mismatch detection
│ │ ├── agents/ # 4 agents
│ │ └── skills/label-taxonomy/
│ ├── git-flow/ # Git workflow automation
│ │ ├── .claude-plugin/plugin.json
│ │ ├── commands/ # 8 commands
│ │ └── agents/
│ ├── pr-review/ # Multi-agent PR review
│ │ ├── .claude-plugin/plugin.json
│ │ ├── .mcp.json
│ │ ├── mcp-servers/gitea -> ../../../mcp-servers/gitea # SYMLINK
│ │ ├── commands/ # 6 commands (incl. setup)
│ │ ├── hooks/ # SessionStart mismatch detection
│ │ └── agents/ # 5 agents
│ ├── clarity-assist/ # Prompt optimization (NEW v3.0.0)
│ │ ├── .claude-plugin/plugin.json
│ │ ├── commands/ # 2 commands
│ │ └── agents/
│ ├── doc-guardian/ # Documentation drift detection
│ ├── code-sentinel/ # Security scanning & refactoring
│ ├── claude-config-maintainer/
│ ├── cmdb-assistant/
│ └── project-hygiene/
├── scripts/
│ ├── setup.sh, post-update.sh
│ └── validate-marketplace.sh # Marketplace compliance validation
└── docs/
├── CANONICAL-PATHS.md # Single source of truth for paths
└── CONFIGURATION.md # Centralized configuration guide
```
## CRITICAL: Rules You MUST Follow
### File Operations
- **NEVER** create files in repository root unless listed in "Allowed Root Files"
- **NEVER** modify `.gitignore` without explicit permission
- **ALWAYS** use `.scratch/` for temporary/exploratory work
- **ALWAYS** verify paths against `docs/CANONICAL-PATHS.md` before creating files
### Plugin Development
- **plugin.json MUST be in `.claude-plugin/` directory** (not plugin root)
- **Every plugin MUST be listed in marketplace.json**
- **MCP servers are SHARED at root** with symlinks from plugins
- **MCP server venv path**: `${CLAUDE_PLUGIN_ROOT}/mcp-servers/{name}/.venv/bin/python`
- **CLI tools forbidden** - Use MCP tools exclusively (never `tea`, `gh`, etc.)
### Hooks (Valid Events Only)
`PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `SessionStart`, `SessionEnd`, `Notification`, `Stop`, `SubagentStop`, `PreCompact`
**INVALID:** `task-completed`, `file-changed`, `git-commit-msg-needed`
### Allowed Root Files
`CLAUDE.md`, `README.md`, `LICENSE`, `CHANGELOG.md`, `.gitignore`, `.env.example`
### Allowed Root Directories
`.claude/`, `.claude-plugin/`, `.claude-plugins/`, `.scratch/`, `docs/`, `hooks/`, `mcp-servers/`, `plugins/`, `scripts/`
## Architecture
### Four-Agent Model (projman)
| Agent | Personality | Responsibilities |
|-------|-------------|------------------|
| **Planner** | Thoughtful, methodical | Sprint planning, architecture analysis, issue creation, lesson search |
| **Orchestrator** | Concise, action-oriented | Sprint execution, parallel batching, Git operations, lesson capture |
| **Executor** | Implementation-focused | Code implementation, branch management, MR creation |
| **Code Reviewer** | Thorough, practical | Pre-close quality review, security scan, test verification |
### MCP Server Tools (Gitea)
| Category | Tools |
|----------|-------|
| Issues | `list_issues`, `get_issue`, `create_issue`, `update_issue`, `add_comment` |
| Labels | `get_labels`, `suggest_labels`, `create_label` |
| Milestones | `list_milestones`, `get_milestone`, `create_milestone`, `update_milestone` |
| Dependencies | `list_issue_dependencies`, `create_issue_dependency`, `get_execution_order` |
| Wiki | `list_wiki_pages`, `get_wiki_page`, `create_wiki_page`, `create_lesson`, `search_lessons` |
| **Pull Requests** | `list_pull_requests`, `get_pull_request`, `get_pr_diff`, `get_pr_comments`, `create_pr_review`, `add_pr_comment` *(NEW v3.0.0)* |
| Validation | `validate_repo_org`, `get_branch_protection` |
### Hybrid Configuration
| Level | Location | Purpose |
|-------|----------|---------|
| System | `~/.config/claude/gitea.env` | Credentials (GITEA_API_URL, GITEA_API_TOKEN) |
| Project | `.env` in project root | Repository specification (GITEA_ORG, GITEA_REPO) |
**Note:** `GITEA_ORG` is at project level since different projects may belong to different organizations.
### Branch-Aware Security
| Branch Pattern | Mode | Capabilities |
|----------------|------|--------------|
| `development`, `feat/*` | Development | Full access |
| `staging` | Staging | Read-only code, can create issues |
| `main`, `master` | Production | Read-only, emergency only |
## Label Taxonomy
43 labels total: 27 organization + 16 repository
**Organization:** Agent/2, Complexity/3, Efforts/5, Priority/4, Risk/3, Source/4, Type/6
**Repository:** Component/9, Tech/7
Sync with `/labels-sync` command.
## Lessons Learned System
Stored in Gitea Wiki under `lessons-learned/sprints/`.
**Workflow:**
1. Orchestrator captures at sprint close via MCP tools
2. Planner searches at sprint start using `search_lessons`
3. Tags enable cross-project discovery
## Common Operations
### Adding a New Plugin
1. Create `plugins/{name}/.claude-plugin/plugin.json`
2. Add entry to `.claude-plugin/marketplace.json` with category, tags, license
3. Create `README.md` and `claude-md-integration.md`
4. If using MCP server, create symlink: `ln -s ../../../mcp-servers/{server} plugins/{name}/mcp-servers/{server}`
5. Run `./scripts/validate-marketplace.sh`
6. Update `CHANGELOG.md`
### Adding a Command to projman
1. Create `plugins/projman/commands/{name}.md`
2. Update `plugins/projman/README.md`
3. Update marketplace description if significant
### Validation
```bash
./scripts/validate-marketplace.sh # Validates all manifests
```
## Path Verification Protocol
**Before creating any file:**
1. Read `docs/CANONICAL-PATHS.md`
2. List all paths to be created/modified
3. Verify each against canonical paths
4. If not in canonical paths, STOP and ask
## Documentation Index
| Document | Purpose |
|----------|---------|
| `docs/CANONICAL-PATHS.md` | **Single source of truth** for paths |
| `docs/COMMANDS-CHEATSHEET.md` | All commands quick reference with workflow examples |
| `docs/CONFIGURATION.md` | Centralized setup guide |
| `docs/UPDATING.md` | Update guide for the marketplace |
| `plugins/projman/CONFIGURATION.md` | Quick reference (links to central) |
| `plugins/projman/README.md` | Projman full documentation |
## Versioning and Changelog Rules
### Version Display
**The marketplace version is displayed ONLY in the main `README.md` title.**
- Format: `# Leo Claude Marketplace - vX.Y.Z`
- Do NOT add version numbers to individual plugin documentation titles
- Do NOT add version numbers to configuration guides
- Do NOT add version numbers to CLAUDE.md or other docs
### Changelog Maintenance (MANDATORY)
**`CHANGELOG.md` is the authoritative source for version history.**
When releasing a new version:
1. Update main `README.md` title with new version
2. Update `CHANGELOG.md` with:
- Version number and date: `## [X.Y.Z] - YYYY-MM-DD`
- **Added**: New features, commands, files
- **Changed**: Modifications to existing functionality
- **Fixed**: Bug fixes
- **Removed**: Deleted features, files, deprecated items
3. Update `marketplace.json` metadata version
4. Update plugin `plugin.json` versions if plugin-specific changes
### Version Format
- Follow [Semantic Versioning](https://semver.org/): MAJOR.MINOR.PATCH
- MAJOR: Breaking changes
- MINOR: New features, backward compatible
- PATCH: Bug fixes, minor improvements
---
**Last Updated:** 2026-01-20

View File

@@ -1,65 +0,0 @@
# Doc Guardian Queue - cleared after sync on 2026-02-02
2026-02-02T11:41:00 | .claude-plugin | /home/lmiranda/claude-plugins-work/.claude-plugin/marketplace.json | CLAUDE.md .claude-plugin/marketplace.json
2026-02-02T13:35:48 | skills | /home/lmiranda/claude-plugins-work/plugins/projman/skills/sprint-approval.md | README.md
2026-02-02T13:36:03 | commands | /home/lmiranda/claude-plugins-work/plugins/projman/commands/sprint-start.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-02T13:36:16 | agents | /home/lmiranda/claude-plugins-work/plugins/projman/agents/orchestrator.md | README.md CLAUDE.md
2026-02-02T13:39:07 | commands | /home/lmiranda/claude-plugins-work/plugins/projman/commands/rfc.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-02T13:39:15 | commands | /home/lmiranda/claude-plugins-work/plugins/projman/commands/setup.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-02T13:39:32 | skills | /home/lmiranda/claude-plugins-work/plugins/projman/skills/rfc-workflow.md | README.md
2026-02-02T13:43:14 | skills | /home/lmiranda/claude-plugins-work/plugins/projman/skills/rfc-templates.md | README.md
2026-02-02T13:44:55 | skills | /home/lmiranda/claude-plugins-work/plugins/projman/skills/sprint-lifecycle.md | README.md
2026-02-02T13:45:04 | skills | /home/lmiranda/claude-plugins-work/plugins/projman/skills/label-taxonomy/labels-reference.md | README.md
2026-02-02T13:45:14 | commands | /home/lmiranda/claude-plugins-work/plugins/projman/commands/sprint-plan.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-02T13:45:48 | commands | /home/lmiranda/claude-plugins-work/plugins/projman/commands/review.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-02T13:46:07 | commands | /home/lmiranda/claude-plugins-work/plugins/projman/commands/sprint-close.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-02T13:46:21 | commands | /home/lmiranda/claude-plugins-work/plugins/projman/commands/sprint-status.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-02T13:46:38 | agents | /home/lmiranda/claude-plugins-work/plugins/projman/agents/planner.md | README.md CLAUDE.md
2026-02-02T13:46:57 | agents | /home/lmiranda/claude-plugins-work/plugins/projman/agents/code-reviewer.md | README.md CLAUDE.md
2026-02-02T13:49:13 | commands | /home/lmiranda/claude-plugins-work/plugins/viz-platform/commands/design-gate.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-02T13:49:24 | commands | /home/lmiranda/claude-plugins-work/plugins/data-platform/commands/data-gate.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-02T13:49:35 | skills | /home/lmiranda/claude-plugins-work/plugins/projman/skills/domain-consultation.md | README.md
2026-02-02T13:50:04 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/contract-validator/mcp_server/validation_tools.py | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-02-02T13:50:59 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/contract-validator/mcp_server/server.py | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-02-02T13:51:32 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/contract-validator/tests/test_validation_tools.py | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-02-02T13:51:49 | skills | /home/lmiranda/claude-plugins-work/plugins/contract-validator/skills/validation-rules.md | README.md
2026-02-02T13:52:07 | skills | /home/lmiranda/claude-plugins-work/plugins/contract-validator/skills/mcp-tools-reference.md | README.md
2026-02-02T13:59:09 | skills | /home/lmiranda/claude-plugins-work/plugins/projman/skills/progress-tracking.md | README.md
2026-02-02T14:01:34 | commands | /home/lmiranda/claude-plugins-work/plugins/projman/commands/test.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:08:38 | commands | /home/lmiranda/claude-plugins-work/plugins/git-flow/commands/git-commit.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:08:39 | commands | /home/lmiranda/claude-plugins-work/plugins/git-flow/commands/git-commit-push.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:08:40 | commands | /home/lmiranda/claude-plugins-work/plugins/git-flow/commands/git-commit-merge.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:08:41 | commands | /home/lmiranda/claude-plugins-work/plugins/git-flow/commands/git-commit-sync.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:08:49 | commands | /home/lmiranda/claude-plugins-work/plugins/cmdb-assistant/commands/cmdb-setup.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:08:50 | commands | /home/lmiranda/claude-plugins-work/plugins/pr-review/commands/project-init.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:08:51 | skills | /home/lmiranda/claude-plugins-work/plugins/cmdb-assistant/skills/visual-header.md | README.md
2026-02-03T21:08:51 | commands | /home/lmiranda/claude-plugins-work/plugins/projman/commands/pm-review.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:08:53 | commands | /home/lmiranda/claude-plugins-work/plugins/projman/commands/pm-test.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:08:54 | skills | /home/lmiranda/claude-plugins-work/plugins/projman/skills/review-checklist.md | README.md
2026-02-03T21:08:55 | skills | /home/lmiranda/claude-plugins-work/plugins/projman/skills/visual-output.md | README.md
2026-02-03T21:08:58 | skills | /home/lmiranda/claude-plugins-work/plugins/projman/skills/setup-workflows.md | README.md
2026-02-03T21:08:59 | skills | /home/lmiranda/claude-plugins-work/plugins/git-flow/skills/sync-workflow.md | README.md
2026-02-03T21:09:00 | skills | /home/lmiranda/claude-plugins-work/plugins/git-flow/skills/commit-conventions.md | README.md
2026-02-03T21:09:00 | skills | /home/lmiranda/claude-plugins-work/plugins/git-flow/skills/merge-workflow.md | README.md
2026-02-03T21:09:08 | commands | /home/lmiranda/claude-plugins-work/plugins/projman/commands/pm-setup.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:09:08 | commands | /home/lmiranda/claude-plugins-work/plugins/data-platform/commands/data-setup.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:09:10 | commands | /home/lmiranda/claude-plugins-work/plugins/data-platform/commands/data-run.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:09:10 | commands | /home/lmiranda/claude-plugins-work/plugins/contract-validator/commands/cv-setup.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:09:11 | commands | /home/lmiranda/claude-plugins-work/plugins/projman/commands/pm-debug.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:09:13 | agents | /home/lmiranda/claude-plugins-work/plugins/contract-validator/agents/full-validation.md | README.md CLAUDE.md
2026-02-03T21:09:14 | commands | /home/lmiranda/claude-plugins-work/plugins/data-platform/commands/data-ingest.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:09:18 | commands | /home/lmiranda/claude-plugins-work/plugins/data-platform/commands/data-profile.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:09:18 | commands | /home/lmiranda/claude-plugins-work/plugins/viz-platform/commands/viz-setup.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:09:20 | commands | /home/lmiranda/claude-plugins-work/plugins/data-platform/commands/data-schema.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:09:20 | commands | /home/lmiranda/claude-plugins-work/plugins/viz-platform/commands/viz-theme.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:09:23 | commands | /home/lmiranda/claude-plugins-work/plugins/viz-platform/commands/viz-theme-new.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:09:24 | commands | /home/lmiranda/claude-plugins-work/plugins/data-platform/commands/data-explain.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:09:26 | commands | /home/lmiranda/claude-plugins-work/plugins/data-platform/commands/data-lineage.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:09:26 | commands | /home/lmiranda/claude-plugins-work/plugins/viz-platform/commands/viz-theme-css.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:09:29 | commands | /home/lmiranda/claude-plugins-work/plugins/viz-platform/commands/viz-chart.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:09:32 | commands | /home/lmiranda/claude-plugins-work/plugins/viz-platform/commands/viz-chart-export.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:09:33 | commands | /home/lmiranda/claude-plugins-work/plugins/data-platform/commands/data-review.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:09:35 | commands | /home/lmiranda/claude-plugins-work/plugins/viz-platform/commands/viz-dashboard.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:09:38 | commands | /home/lmiranda/claude-plugins-work/plugins/viz-platform/commands/viz-component.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:09:40 | commands | /home/lmiranda/claude-plugins-work/plugins/viz-platform/commands/viz-breakpoints.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:09:46 | commands | /home/lmiranda/claude-plugins-work/plugins/viz-platform/commands/design-review.md | docs/COMMANDS-CHEATSHEET.md README.md
2026-02-03T21:10:22 | commands | /home/lmiranda/claude-plugins-work/plugins/viz-platform/commands/accessibility-check.md | docs/COMMANDS-CHEATSHEET.md README.md

1
.env
View File

@@ -1 +0,0 @@
GITEA_REPO=personal-projects/leo-claude-mktplace

9
.gitignore vendored
View File

@@ -31,8 +31,6 @@ venv/
ENV/
env/
.venv/
.venv
**/.venv
# PyCharm
.idea/
@@ -84,13 +82,6 @@ Thumbs.db
# Claude Code
.claude/settings.local.json
.claude/history/
.claude/backups/
# Doc Guardian transient files
.doc-guardian-queue
# Development convenience links
.marketplaces-link
# Logs
logs/

View File

@@ -1,24 +0,0 @@
{
"mcpServers": {
"gitea": {
"command": "./mcp-servers/gitea/run.sh",
"args": []
},
"netbox": {
"command": "./mcp-servers/netbox/run.sh",
"args": []
},
"viz-platform": {
"command": "./mcp-servers/viz-platform/run.sh",
"args": []
},
"data-platform": {
"command": "./mcp-servers/data-platform/run.sh",
"args": []
},
"contract-validator": {
"command": "./mcp-servers/contract-validator/run.sh",
"args": []
}
}
}

View File

@@ -8,684 +8,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
---
## [7.0.0] - 2026-02-03
### BREAKING CHANGES
#### Command Namespace Rename
All generic command names are now prefixed with their plugin's namespace to eliminate collisions across the marketplace. This is a **breaking change** for consuming projects — update your CLAUDE.md integration snippets.
**Full Rename Map:**
| Plugin | Old | New |
|--------|-----|-----|
| projman | `/setup` | `/pm-setup` |
| projman | `/review` | `/pm-review` |
| projman | `/test` | `/pm-test` |
| projman | `/debug` | `/pm-debug` |
| git-flow | `/commit` | `/git-commit` |
| git-flow | `/commit-push` | `/git-commit-push` |
| git-flow | `/commit-merge` | `/git-commit-merge` |
| git-flow | `/commit-sync` | `/git-commit-sync` |
| pr-review | `/initial-setup` | `/pr-setup` |
| cmdb-assistant | `/initial-setup` | `/cmdb-setup` |
| data-platform | `/initial-setup` | `/data-setup` |
| data-platform | `/run` | `/data-run` |
| data-platform | `/ingest` | `/data-ingest` |
| data-platform | `/profile` | `/data-profile` |
| data-platform | `/schema` | `/data-schema` |
| data-platform | `/explain` | `/data-explain` |
| data-platform | `/lineage` | `/data-lineage` |
| viz-platform | `/initial-setup` | `/viz-setup` |
| viz-platform | `/theme` | `/viz-theme` |
| viz-platform | `/theme-new` | `/viz-theme-new` |
| viz-platform | `/theme-css` | `/viz-theme-css` |
| viz-platform | `/chart` | `/viz-chart` |
| viz-platform | `/chart-export` | `/viz-chart-export` |
| viz-platform | `/dashboard` | `/viz-dashboard` |
| viz-platform | `/component` | `/viz-component` |
| viz-platform | `/breakpoints` | `/viz-breakpoints` |
| contract-validator | `/initial-setup` | `/cv-setup` |
**Migration:** Update your project's CLAUDE.md integration snippets to use the new command names. Run `/plugin list` to verify installed plugins are using v7.0.0+.
**Unchanged:** Commands already using plugin-namespaced prefixes (`/sprint-*`, `/cmdb-*`, `/labels-sync`, `/branch-*`, `/git-status`, `/git-config`, `/pr-review`, `/pr-summary`, `/pr-findings`, `/pr-diff`, `/project-init`, `/project-sync`, `/config-*`, `/design-*`, `/data-quality`, `/data-review`, `/data-gate`, `/lineage-viz`, `/dbt-test`, `/accessibility-check`, `/validate-contracts`, `/check-agent`, `/list-interfaces`, `/dependency-graph`, `/doc-audit`, `/doc-sync`, `/security-scan`, `/refactor`, `/refactor-dry`, `/clarify`, `/suggest-version`, `/proposal-status`, `/rfc`, `/change-audit`, `/ip-conflicts`) are **not affected**.
### Added
#### Plan-Then-Batch Skill Optimization (projman)
New execution pattern that separates cognitive work from mechanical API operations, reducing skill-related token consumption by ~76-83% during sprint workflows.
- **`skills/batch-execution.md`** — New skill defining the plan-then-batch protocol:
- Phase 1: Cognitive work with all skills loaded
- Phase 2: Execution manifest (structured plan of all API operations)
- Phase 3: Batch execute API calls using only frontmatter skills
- Phase 4: Batch report with success/failure summary
- Error handling: continue on individual failures, report at end
- **Frontmatter skill promotion:**
- Planner agent: `mcp-tools-reference` and `batch-execution` promoted to frontmatter (auto-injected, zero re-read cost)
- Orchestrator agent: same promotion
- Eliminates per-operation skill file re-reads during API execution loops
- **Phase-based skill loading:**
- Planner: 3 phases (validation → analysis → approval) with explicit "read once" instructions
- Orchestrator: 2 phases (startup → dispatch) with same pattern
- New `## Skill Loading Protocol` section replaces flat `## Skills to Load` in agent files
### Changed
- **`planning-workflow.md`** — Steps 8-10 restructured:
- Step 8: "Draft Issue Specifications" (no API calls — resolve all parameters first)
- Step 8a: "Batch Execute Issue Creation" (tight API loop, frontmatter skills only)
- Step 9: Merged into Step 8a (dependencies created in batch)
- Step 10: Milestone creation moved before batch (must exist for assignment)
- **Agent matrix updated:**
- Planner: `body text (14)``frontmatter (2) + body text (12)`
- Orchestrator: `body text (12)``frontmatter (2) + body text (10)`
- **`docs/CONFIGURATION.md`** — New "Phase-Based Skill Loading" subsection documenting the pattern
### Token Impact
| Scenario | Before | After | Savings |
|----------|--------|-------|---------|
| 6-issue sprint (planning) | ~23,800 lines | ~5,600 lines | ~76% |
| 10-issue sprint (planning) | ~35,000 lines | ~7,000 lines | ~80% |
| 8-issue status updates (orchestrator) | ~9,600 lines | ~1,600 lines | ~83% |
---
## [5.10.0] - 2026-02-03
### Added
#### NetBox MCP Server: Module-Based Tool Filtering
Environment-variable-driven module filtering to reduce token consumption:
- **New config option**: `NETBOX_ENABLED_MODULES` in `~/.config/claude/netbox.env`
- **Token savings**: ~15,000 tokens (from ~19,810 to ~4,500) with recommended config
- **Default behavior**: All modules enabled if env var unset (backward compatible)
- **Startup logging**: Shows enabled modules and tool count on initialization
- **Routing guard**: Clear error message when calling disabled module's tools
**Recommended configuration for cmdb-assistant users:**
```bash
NETBOX_ENABLED_MODULES=dcim,ipam,virtualization,extras
```
This enables ~43 tools covering all cmdb-assistant commands while staying well below the 25K token warning threshold.
### Fixed
#### cmdb-assistant Documentation: Incorrect Tool Names
Fixed documentation referencing non-existent `virtualization_*` tool names:
| File | Wrong | Correct |
|------|-------|---------|
| `claude-md-integration.md` | `virtualization_list_virtual_machines` | `virt_list_vms` |
| `claude-md-integration.md` | `virtualization_create_virtual_machine` | `virt_create_vm` |
| `cmdb-search.md` | `virtualization_list_virtual_machines` | `virt_list_vms` |
Also fixed NetBox README.md tool name references for virtualization, wireless, and circuits modules.
#### Gitea MCP Server: Standardized Build Backend
Changed `mcp-servers/gitea/pyproject.toml` from hatchling to setuptools:
- Matches all other MCP servers (contract-validator, viz-platform, data-platform)
- Updated license format to PEP 639 compliance
- Added pytest configuration for consistency
---
## [5.9.0] - 2026-02-03
### Added
#### Plugin Installation Scripts
New scripts for installing marketplace plugins into consumer projects:
- **`scripts/install-plugin.sh`** — Install a plugin to a consumer project
- Adds MCP server entry to target's `.mcp.json` (if plugin has MCP server)
- Appends integration snippet to target's `CLAUDE.md`
- Idempotent: safe to run multiple times
- Validates plugin exists and target path is valid
- **`scripts/uninstall-plugin.sh`** — Remove a plugin from a consumer project
- Removes MCP server entry from `.mcp.json`
- Removes integration section from `CLAUDE.md`
- **`scripts/list-installed.sh`** — Show installed plugins in a project
- Lists fully installed, partially installed, and available plugins
- Shows plugin versions and descriptions
**Usage:**
```bash
./scripts/install-plugin.sh data-platform ~/projects/personal-portfolio
./scripts/list-installed.sh ~/projects/personal-portfolio
./scripts/uninstall-plugin.sh data-platform ~/projects/personal-portfolio
```
**Documentation:** `docs/CONFIGURATION.md` updated with "Installing Plugins to Consumer Projects" section.
### Fixed
#### Plugin Installation Scripts — MCP Mapping & Section Markers
**MCP Server Mapping:**
- Added `mcp_servers` field to plugin.json for plugins that use shared MCP servers
- `projman` and `pr-review` now correctly install `gitea` MCP server
- `cmdb-assistant` now correctly installs `netbox` MCP server
- Scripts read MCP server names from plugin.json instead of assuming plugin name = server name
**CLAUDE.md Section Markers:**
- Install script now wraps integration content with HTML comment markers:
`<!-- BEGIN marketplace-plugin: {name} -->` and `<!-- END marketplace-plugin: {name} -->`
- Uninstall script uses markers for precise section removal (no more code block false positives)
- Backward compatible: falls back to legacy header detection for pre-marker installations
**Plugins updated with `mcp_servers` field:**
- `projman``["gitea"]`
- `pr-review``["gitea"]`
- `cmdb-assistant``["netbox"]`
- `data-platform``["data-platform"]`
- `viz-platform``["viz-platform"]`
- `contract-validator``["contract-validator"]`
#### Agent Model Selection
Per-agent model selection using Claude Code's now-supported `model` frontmatter field.
- All 25 marketplace agents assigned appropriate model (`sonnet`, `haiku`, or `inherit`)
- Model assignment based on reasoning depth, tool complexity, and latency requirements
- Documentation added to `CLAUDE.md` and `docs/CONFIGURATION.md`
**Supported values:** `sonnet` (default), `opus`, `haiku`, `inherit`
**Model assignments:**
| Model | Agent Types |
|-------|-------------|
| sonnet | Planner, Orchestrator, Executor, Code Reviewer, Coordinator, Security Reviewers, Data Advisor, Design Reviewer, etc. |
| haiku | Maintainability Auditor, Test Validator, Component Check, Theme Setup, Git Assistant, Data Ingestion, Agent Check |
### Fixed
#### Agent Frontmatter Standardization
- Fixed viz-platform and data-platform agents using non-standard `agent:` field (now `name:`)
- Removed non-standard `triggers:` field from domain agents (trigger info already in agent body)
- Added missing frontmatter to 13 agents across pr-review, viz-platform, contract-validator, clarity-assist, git-flow, doc-guardian, code-sentinel, cmdb-assistant, and data-platform
- All 25 agents now have consistent `name`, `description`, and `model` fields
### Changed
#### Agent Frontmatter Hardening v3
Comprehensive agent-level configuration using Claude Code's supported frontmatter fields.
**permissionMode added to all 25 agents:**
- `bypassPermissions` (1): Executor — full autonomy with code-sentinel + Code Reviewer safety nets
- `acceptEdits` (7): Orchestrator, Data Ingestion, Theme Setup, Refactor Advisor, Doc Analyzer, Git Assistant, Maintainer
- `default` (7): Planner, Code Reviewer, Data Advisor, Layout Builder, Full Validation, Clarity Coach, CMDB Assistant
- `plan` (10): All pr-review agents (5), Data Analysis, Design Reviewer, Component Check, Agent Check, Security Reviewer (code-sentinel)
**disallowedTools added to 12 agents:**
- All `plan`-mode agents (10) + Code Reviewer + Clarity Coach receive `disallowedTools: Write, Edit, MultiEdit`
- Enforces read-only contracts at platform level (defense-in-depth with `permissionMode`)
**Model promotions:**
- Planner: `sonnet``opus` (architectural reasoning benefits from deeper analysis)
- Code Reviewer: `sonnet``opus` (quality gate benefits from thorough review)
**skills frontmatter on 3 agents:**
- Executor: 7 safety-critical skills auto-injected (branch-security, runaway-detection, etc.)
- Code Reviewer: 4 review skills auto-injected
- Maintainer: 2 config skills auto-injected
- Body text `## Skills to Load` removed for these agents to avoid duplication
**Documentation:**
- `CLAUDE.md` and `docs/CONFIGURATION.md` updated with complete agent configuration matrix
- New subsections: permissionMode Guide, disallowedTools Guide, skills Frontmatter Guide
---
## [5.8.0] - 2026-02-02
### Added
#### claude-config-maintainer v1.2.0 - Settings Audit Feature
New commands for auditing and optimizing `settings.local.json` permission configurations:
- **`/config-audit-settings`** — Audit `settings.local.json` permissions with 100-point scoring across redundancy, coverage, safety alignment, and profile fit
- **`/config-optimize-settings`** — Apply permission optimizations with dry-run, named profiles (`conservative`, `reviewed`, `autonomous`), and consolidation modes
- **`/config-permissions-map`** — Generate Mermaid diagram of review layer coverage and permission gaps
- **`skills/settings-optimization.md`** — Comprehensive skill for permission pattern analysis, consolidation rules, review-layer-aware recommendations, and named profiles
**Key Features:**
- Settings Efficiency Score (100 points) alongside existing CLAUDE.md score
- Review layer verification — agent reads `hooks/hooks.json` from installed plugins before recommending auto-allow patterns
- Three named profiles: `conservative` (prompts for most writes), `reviewed` (for projects with ≥2 review layers), `autonomous` (sandboxed environments)
- Pattern consolidation detection: duplicates, subsets, merge candidates, stale entries, conflicts
#### Projman Hardening Sprint
Targeted improvements to safety gates, command structure, lifecycle tracking, and cross-plugin contracts.
**Sprint Lifecycle State Machine:**
- New `skills/sprint-lifecycle.md` - defines valid states and transitions via milestone metadata
- States: idle -> Sprint/Planning -> Sprint/Executing -> Sprint/Reviewing -> idle
- All sprint commands check and set lifecycle state on entry/exit
- Out-of-order calls produce warnings with guidance, `--force` override available
**Sprint Dispatch Log:**
- Orchestrator now maintains a structured dispatch log during execution
- Records task dispatch, completion, failure, gate checks, and resume events
- Enables timeline reconstruction after interrupted sessions
**Gate Contract Versioning:**
- Gate commands (`/design-gate`, `/data-gate`) declare `gate_contract: v1` in frontmatter
- `domain-consultation.md` Gate Command Reference includes expected contract version
- `validate_workflow_integration` now checks contract version compatibility
- Mismatch produces WARNING, missing contract produces INFO suggestion
**Shared Visual Output Skill:**
- New `skills/visual-output.md` - single source of truth for projman visual headers
- All 4 agent files reference the skill instead of inline templates
- Phase Registry maps agents to emoji and phase names
### Changed
**Sprint Approval Gate Hardened:**
- Approval is now a hard block, not a warning (was "recommended", now required)
- `--force` flag added to bypass in emergencies (logged to milestone)
- Consistent language across sprint-approval.md, sprint-start.md, and orchestrator.md
**RFC Commands Normalized:**
- 5 individual commands (`/rfc-create`, `/rfc-list`, `/rfc-review`, `/rfc-approve`, `/rfc-reject`) consolidated into `/rfc create|list|review|approve|reject`
- `/clear-cache` absorbed into `/setup --clear-cache`
- Command count reduced from 17 to 12
**`/test` Command Documentation Expanded:**
- Sprint integration section (pre-close verification workflow)
- Concrete usage examples for all modes
- Edge cases table
- DO NOT rules for both modes
### Removed
- `plugins/projman/commands/rfc-create.md` (replaced by `/rfc create`)
- `plugins/projman/commands/rfc-list.md` (replaced by `/rfc list`)
- `plugins/projman/commands/rfc-review.md` (replaced by `/rfc review`)
- `plugins/projman/commands/rfc-approve.md` (replaced by `/rfc approve`)
- `plugins/projman/commands/rfc-reject.md` (replaced by `/rfc reject`)
- `plugins/projman/commands/clear-cache.md` (replaced by `/setup --clear-cache`)
---
## [5.7.1] - 2026-02-02
### Added
- **contract-validator**: New `validate_workflow_integration` MCP tool — validates domain plugins expose required advisory interfaces (gate command, review command, advisory agent)
- **contract-validator**: New `MISSING_INTEGRATION` issue type for workflow integration validation
### Fixed
- `scripts/setup.sh` banner version updated from v5.1.0 to v5.7.1
### Reverted
- **marketplace.json**: Removed `integrates_with` field — Claude Code schema does not support custom plugin fields (causes marketplace load failure)
---
## [5.7.0] - 2026-02-02
### Added
- **data-platform**: New `data-advisor` agent for data integrity, schema, and dbt compliance validation
- **data-platform**: New `data-integrity-audit.md` skill defining audit rules, severity levels, and scanning strategies
- **data-platform**: New `/data-gate` command for binary pass/fail data integrity gates (projman integration)
- **data-platform**: New `/data-review` command for comprehensive data integrity audits
### Changed
- Domain Advisory Pattern now fully operational for both Viz and Data domains
- projman orchestrator `Domain/Data` gates now resolve to live `/data-gate` command (previously fell through to "gate unavailable" warning)
---
## [5.6.0] - 2026-02-01
### Added
- **Domain Advisory Pattern**: Cross-plugin integration enabling projman to consult domain-specific plugins during sprint lifecycle
- **projman**: New `domain-consultation.md` skill for domain detection and gate protocols
- **viz-platform**: New `design-reviewer` agent for design system compliance auditing
- **viz-platform**: New `design-system-audit.md` skill defining audit rules and severity levels
- **viz-platform**: New `/design-review` command for detailed design system audits
- **viz-platform**: New `/design-gate` command for binary pass/fail validation gates
- **Labels**: New `Domain/Viz` and `Domain/Data` labels for domain routing
### Changed
- **projman planner**: Now loads domain-consultation skill and performs domain detection during planning
- **projman orchestrator**: Now runs domain gates before marking Domain/* labeled issues as complete
---
## [5.5.0] - 2026-02-01
### Added
#### RFC System for Feature Tracking
Wiki-based Request for Comments (RFC) system for capturing, reviewing, and tracking feature ideas through their lifecycle.
**New Commands (projman):**
- `/rfc-create` - Create new RFC from conversation or clarified specification
- `/rfc-list` - List all RFCs grouped by status (Draft, Review, Approved, Implementing, Implemented, Rejected, Stale)
- `/rfc-review` - Submit Draft RFC for maintainer review
- `/rfc-approve` - Approve RFC, making it available for sprint planning
- `/rfc-reject` - Reject RFC with documented reason
**RFC Lifecycle:**
- Draft → Review → Approved → Implementing → Implemented
- Terminal states: Rejected, Superseded
- Stale: Drafts with no activity >90 days
**Sprint Integration:**
- `/sprint-plan` now detects approved RFCs and offers selection
- `/sprint-close` updates RFC status to Implemented on completion
- RFC-Index wiki page auto-maintained with status sections
**Clarity-Assist Integration:**
- Vagueness hook now detects feature request patterns
- Suggests `/rfc-create` for feature ideas
- `/clarify` offers RFC creation after delivering clarified spec
**New MCP Tool:**
- `allocate_rfc_number` - Allocates next sequential RFC number
**New Skills:**
- `skills/rfc-workflow.md` - RFC lifecycle and state transitions
- `skills/rfc-templates.md` - RFC page template specifications
### Changed
#### Sprint 8: Hook Efficiency Quick Wins
Performance optimizations for plugin hooks to reduce overhead on every command.
**Changes:**
- **viz-platform:** Remove SessionStart hook that only echoed "loaded" (zero value)
- **git-flow:** Add early exit to `branch-check.sh` for non-git commands (skip JSON parsing)
- **git-flow:** Add early exit to `commit-msg-check.sh` for non-git commands (skip Python spawn)
- **project-hygiene:** Add 60-second cooldown to `cleanup.sh` (reduce find operations)
**Impact:** Hooks now exit immediately for 90%+ of Bash commands that don't need processing.
**Issues:** #321, #322, #323, #324
**PR:** #334
---
## [5.4.1] - 2026-01-30
### Removed
#### Multi-Model Agent Support (REVERTED)
**Reason:** Claude Code does not support `defaultModel` in plugin.json or `model` in agent frontmatter. The schema validation rejects these as "Unrecognized key".
**Removed:**
- `defaultModel` field from all plugin.json files (6 plugins)
- `model` field references from agent frontmatter
- `docs/MODEL-RECOMMENDATIONS.md` - Deleted entirely
- Model configuration sections from `docs/CONFIGURATION.md` and `CLAUDE.md`
**Lesson:** Do not implement features without verifying they are supported by Claude Code's plugin schema.
---
## [5.4.0] - 2026-01-28 [REVERTED]
### Added (NOW REMOVED - See 5.4.1)
#### Sprint 7: Multi-Model Agent Support
~~Configurable model selection for agents with inheritance chain.~~
**This feature was reverted in 5.4.1 - Claude Code does not support these fields.**
Original sprint work:
- Issues: #302, #303, #304, #305, #306
- PRs: #307, #308
---
## [5.3.0] - 2026-01-28
### Added
#### Sprint 6: Visual Branding Overhaul
Consistent visual headers and progress tracking across all plugins.
**Visual Output Headers (109 files):**
- **Projman**: Double-line headers (╔═╗) with phase indicators (🎯 PLANNING, ⚡ EXECUTION, 🏁 CLOSING)
- **Other Plugins**: Single-line headers (┌─┐) with plugin icons
- **All 23 agents** updated with Visual Output Requirements section
- **All 86 commands** updated with Visual Output section and header templates
**Plugin Icon Registry:**
| Plugin | Icon |
|--------|------|
| projman | 📋 |
| code-sentinel | 🔒 |
| doc-guardian | 📝 |
| pr-review | 🔍 |
| clarity-assist | 💬 |
| git-flow | 🔀 |
| cmdb-assistant | 🖥️ |
| data-platform | 📊 |
| viz-platform | 🎨 |
| contract-validator | ✅ |
| claude-config-maintainer | ⚙️ |
**Wiki Branding Specification (4 pages):**
- [branding/visual-spec](https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/wiki/branding%2Fvisual-spec) - Central specification
- [branding/plugin-registry](https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/wiki/branding%2Fplugin-registry) - Icons and styles
- [branding/header-templates](https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/wiki/branding%2Fheader-templates) - Copy-paste templates
- [branding/progress-templates](https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/wiki/branding%2Fprogress-templates) - Sprint progress blocks
### Fixed
- **Docs:** Version sync - CLAUDE.md, marketplace.json, README.md now consistent
- **Docs:** Added 18 missing commands from Sprint 4 & 5 to README.md and COMMANDS-CHEATSHEET.md
- **MCP:** Registered `/sprint-diagram` as invokable skill
**Sprint Completed:**
- Milestone: Sprint 6 - Visual Branding Overhaul (closed 2026-01-28)
- Issues: #272, #273, #274, #275, #276, #277, #278
- PRs: #284, #285
- Wiki: [Sprint 6 Lessons](https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/wiki/lessons/sprints/sprint-6---visual-branding-and-documentation-maintenance)
---
## [5.2.0] - 2026-01-28
### Added
#### Sprint 5: Documentation (V5.2.0 Plugin Enhancements)
Documentation and guides for the plugin enhancements initiative.
**git-flow v1.2.0:**
- **Branching Strategy Guide** (`docs/BRANCHING-STRATEGY.md`) - Complete documentation of `development -> staging -> main` promotion flow with Mermaid diagrams
**clarity-assist v1.2.0:**
- **ND Support Guide** (`docs/ND-SUPPORT.md`) - Documentation of neurodivergent accommodations, features, and usage examples
**Gitea MCP Server:**
- **`update_issue` milestone parameter** - Can now assign/change milestones programmatically
**Sprint Completed:**
- Milestone: Sprint 5 - Documentation (closed 2026-01-28)
- Issues: #266, #267, #268, #269
- Wiki: [Change V5.2.0: Plugin Enhancements (Sprint 5 Documentation)](https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/wiki/Change-V5.2.0%3A-Plugin-Enhancements-%28Sprint-5-Documentation%29)
---
#### Sprint 4: Commands (V5.2.0 Plugin Enhancements)
Implementation of 18 new user-facing commands across 8 plugins.
**projman v3.3.0:**
- **`/sprint-diagram`** - Generate Mermaid diagram of sprint issues with dependencies and status
**pr-review v1.1.0:**
- **`/pr-diff`** - Formatted diff with inline review comments and annotations
- **Confidence threshold config** - `PR_REVIEW_CONFIDENCE_THRESHOLD` env var (default: 0.7)
**data-platform v1.2.0:**
- **`/data-quality`** - DataFrame quality checks (nulls, duplicates, types, outliers) with pass/warn/fail scoring
- **`/lineage-viz`** - dbt lineage visualization as Mermaid diagrams
- **`/dbt-test`** - Formatted dbt test runner with summary and failure details
**viz-platform v1.1.0:**
- **`/chart-export`** - Export charts to PNG, SVG, PDF via kaleido
- **`/accessibility-check`** - Color blind validation (WCAG contrast ratios)
- **`/breakpoints`** - Responsive layout breakpoint configuration
- **New MCP tools**: `chart_export`, `accessibility_validate_colors`, `accessibility_validate_theme`, `accessibility_suggest_alternative`, `layout_set_breakpoints`
- **New dependency**: kaleido>=0.2.1 for chart rendering
**contract-validator v1.2.0:**
- **`/dependency-graph`** - Mermaid visualization of plugin dependencies with data flow
**doc-guardian v1.1.0:**
- **`/changelog-gen`** - Generate changelog from conventional commits
- **`/doc-coverage`** - Documentation coverage metrics by function/class
- **`/stale-docs`** - Flag documentation behind code changes
**claude-config-maintainer v1.1.0:**
- **`/config-diff`** - Track CLAUDE.md changes over time with behavioral impact analysis
- **`/config-lint`** - 31 lint rules for CLAUDE.md (security, structure, content, format, best practices)
**cmdb-assistant v1.2.0:**
- **`/cmdb-topology`** - Infrastructure topology diagrams (rack, network, site views)
- **`/change-audit`** - NetBox audit trail queries with filtering
- **`/ip-conflicts`** - Detect IP conflicts and overlapping prefixes
**Sprint Completed:**
- Milestone: Sprint 4 - Commands (closed 2026-01-28)
- Issues: #241-#258 (18/18 closed)
- Wiki: [Change V5.2.0: Plugin Enhancements (Sprint 4 Commands)](https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/wiki/Change-V5.2.0%3A-Plugin-Enhancements-%28Sprint-4-Commands%29)
- Lessons: [Sprint 4 - Plugin Commands Implementation](https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/wiki/lessons/sprints/sprint-4---plugin-commands-implementation)
### Fixed
- **MCP:** Project directory detection - all run.sh scripts now capture `CLAUDE_PROJECT_DIR` from PWD before changing directories
- **Docs:** Added Gitea auto-close behavior and MCP session restart notes to DEBUGGING-CHECKLIST.md
---
#### Sprint 3: Hooks (V5.2.0 Plugin Enhancements)
Implementation of 6 foundational hooks across 4 plugins.
**git-flow v1.1.0:**
- **Commit message enforcement hook** - PreToolUse hook validates conventional commit format on all `git commit` commands (not just `/commit`). Blocks invalid commits with format guidance.
- **Branch name validation hook** - PreToolUse hook validates branch naming on `git checkout -b` and `git switch -c`. Enforces `type/description` format, lowercase, max 50 chars.
**clarity-assist v1.1.0:**
- **Vagueness detection hook** - UserPromptSubmit hook detects vague prompts and suggests `/clarify` when ambiguity, missing context, or unclear scope detected.
**data-platform v1.1.0:**
- **Schema diff detection hook** - PostToolUse hook monitors edits to schema files (dbt models, SQL migrations). Warns on breaking changes (column removal, type narrowing, constraint addition).
**contract-validator v1.1.0:**
- **SessionStart auto-validate hook** - Smart validation that only runs when plugin files changed since last check. Detects interface compatibility issues at session start.
- **Breaking change detection hook** - PostToolUse hook monitors plugin interface files (README.md, plugin.json). Warns when changes would break consumers.
**Sprint Completed:**
- Milestone: Sprint 3 - Hooks (closed 2026-01-28)
- Issues: #225, #226, #227, #228, #229, #230
- Wiki: [Change V5.2.0: Plugin Enhancements Proposal](https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/wiki/Change-V5.2.0:-Plugin-Enhancements-Proposal)
- Lessons: Background agent permissions, agent runaway detection, MCP branch detection bug
### Known Issues
- **MCP Bug #231:** Branch detection in Gitea MCP runs from installed plugin directory, not user's project directory. Workaround: close issues via Gitea web UI.
---
#### Gitea MCP Server - create_pull_request Tool
- **`create_pull_request`**: Create new pull requests via MCP
- Parameters: title, body, head (source branch), base (target branch), labels
- Branch-aware security: only allowed on development/feature branches
- Completes the PR lifecycle (was previously missing - only had list/get/review/comment)
#### cmdb-assistant v1.1.0 - Data Quality Validation
- **SessionStart Hook**: Tests NetBox API connectivity at session start
- Warns if VMs exist without site assignment
- Warns if devices exist without platform
- Non-blocking: displays warning, doesn't prevent work
- **PreToolUse Hook**: Validates input parameters before VM/device operations
- Warns about missing site, tenant, platform
- Non-blocking: suggests best practices without blocking
- **`/cmdb-audit` Command**: Comprehensive data quality analysis
- Scopes: all, vms, devices, naming, roles
- Identifies Critical/High/Medium/Low issues
- Provides prioritized remediation recommendations
- **`/cmdb-register` Command**: Register current machine into NetBox
- Discovers system info: hostname, platform, hardware, network interfaces
- Discovers running apps: Docker containers, systemd services
- Creates device with interfaces, IPs, and sets primary IP
- Creates cluster and VMs for Docker containers
- **`/cmdb-sync` Command**: Sync machine state with NetBox
- Compares current state with NetBox record
- Shows diff of changes (interfaces, IPs, containers)
- Updates with user confirmation
- Supports --full and --dry-run flags
- **NetBox Best Practices Skill**: Reference documentation
- Dependency order for object creation
- Naming conventions (`{role}-{site}-{number}`, `{env}-{app}-{number}`)
- Role consolidation guidance
- Site/tenant/platform assignment requirements
- **Agent Enhancement**: Updated cmdb-assistant agent with validation requirements
- Proactive suggestions for missing fields
- Naming convention checks
- Dependency order enforcement
- Duplicate prevention
---
## [5.0.0] - 2026-01-26
### Added
#### Sprint 1: viz-platform Plugin ✅ Completed
- **viz-platform** v1.0.0 - Visualization tools with Dash Mantine Components validation and theming
- **DMC Tools** (3 tools): `list_components`, `get_component_props`, `validate_component`
- Version-locked component registry prevents Claude from hallucinating invalid props
- Static JSON registry approach for fast, deterministic validation
- **Chart Tools** (2 tools): `chart_create`, `chart_configure_interaction`
- Plotly-based visualization with theme token support
- **Layout Tools** (5 tools): `layout_create`, `layout_add_filter`, `layout_set_grid`, `layout_get`, `layout_add_section`
- Dashboard composition with responsive grid system
- **Theme Tools** (6 tools): `theme_create`, `theme_extend`, `theme_validate`, `theme_export_css`, `theme_list`, `theme_activate`
- Design token-based theming system
- Dual storage: user-level (`~/.config/claude/themes/`) and project-level
- **Page Tools** (5 tools): `page_create`, `page_add_navbar`, `page_set_auth`, `page_list`, `page_get_app_config`
- Multi-page Dash app structure generation
- **Commands**: `/chart`, `/dashboard`, `/theme`, `/theme-new`, `/theme-css`, `/component`, `/initial-setup`
- **Agents**: `theme-setup`, `layout-builder`, `component-check`
- **SessionStart Hook**: DMC version check (non-blocking)
- **Tests**: 94 tests passing
- config.py: 82% coverage
- component_registry.py: 92% coverage
- dmc_tools.py: 88% coverage
- chart_tools.py: 68% coverage
- theme_tools.py: 99% coverage
**Sprint Completed:**
- Milestone: Sprint 1 - viz-platform Plugin (closed 2026-01-26)
- Issues: #170-#182 (13/13 closed)
- Wiki: [Sprint-1-viz-platform-Implementation-Plan](https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/wiki/Sprint-1-viz-platform-Implementation-Plan)
- Lessons: [sprint-1---viz-platform-plugin-implementation](https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/wiki/lessons/sprints/sprint-1---viz-platform-plugin-implementation)
- Reference: `docs/changes/CHANGE_V04_0_0_PROPOSAL_ORIGINAL.md` (Phases 4-5)
---
## [4.1.0] - 2026-01-26
### Added

327
CLAUDE.md
View File

@@ -1,5 +1,6 @@
# CLAUDE.md
This file provides guidance to Claude Code when working with code in this repository.
## ⛔ MANDATORY BEHAVIOR RULES - READ FIRST
**These rules are NON-NEGOTIABLE. Violating them wastes the user's time and money.**
@@ -8,162 +9,63 @@
- Search ALL locations, not just where you think it is
- Check cache directories: `~/.claude/plugins/cache/`
- Check installed: `~/.claude/plugins/marketplaces/`
- Check source directories
- Check source: `~/claude-plugins-work/`
- **NEVER say "no" or "that's not the issue" without exhaustive verification**
### 2. WHEN USER SAYS SOMETHING IS WRONG - BELIEVE THEM
- The user knows their system better than you
- Investigate thoroughly before disagreeing
- If user suspects cache, CHECK THE CACHE
- If user suspects a file, READ THE FILE
- **Your confidence is often wrong. User's instincts are often right.**
### 3. NEVER SAY "DONE" WITHOUT VERIFICATION
- Run the actual command/script to verify
- Show the output to the user
- Check ALL affected locations
- **"Done" means VERIFIED WORKING, not "I made changes"**
### 4. SHOW EXACTLY WHAT USER ASKS FOR
- If user asks for messages, show the MESSAGES
- If user asks for code, show the CODE
- **Do not interpret or summarize unless asked**
- If user asks for output, show the OUTPUT
- **Don't interpret or summarize unless asked**
### 5. AFTER PLUGIN UPDATES - VERIFY AND RESTART
**⚠️ DO NOT clear cache mid-session** - this breaks MCP tools that are already loaded.
1. Run `./scripts/verify-hooks.sh` to check hook types
2. If changes affect MCP servers or hooks, inform the user:
> "Plugin changes require a session restart to take effect. Please restart Claude Code."
3. Cache clearing is ONLY safe **before** starting a new session (not during)
See `docs/DEBUGGING-CHECKLIST.md` for details on cache timing.
**FAILURE TO FOLLOW THESE RULES = WASTED USER TIME = UNACCEPTABLE**
---
This file provides guidance to Claude Code when working with code in this repository.
## ⛔ RULES - READ FIRST
### Behavioral Rules
| Rule | Summary |
|------|---------|
| **Check everything** | Search cache (`~/.claude/plugins/cache/`), installed (`~/.claude/plugins/marketplaces/`), and source (`~/claude-plugins-work/`) |
| **Believe the user** | User knows their system. Investigate before disagreeing. |
| **Verify before "done"** | Run commands, show output, check all locations. "Done" = verified working. |
| **Show what's asked** | Don't interpret or summarize unless asked. |
### After Plugin Updates
Run `./scripts/verify-hooks.sh`. If changes affect MCP servers or hooks, inform user to restart session.
**DO NOT clear cache mid-session** - breaks loaded MCP tools.
### NEVER USE CLI TOOLS FOR EXTERNAL SERVICES
- **FORBIDDEN:** `gh`, `tea`, `curl` to APIs, any CLI that talks to Gitea/GitHub/external services
- **REQUIRED:** Use MCP tools exclusively (`mcp__plugin_projman_gitea__*`, `mcp__plugin_pr-review_gitea__*`)
- **NO EXCEPTIONS.** Don't try CLI first. Don't fall back to CLI. MCP ONLY.
### NEVER PUSH DIRECTLY TO PROTECTED BRANCHES
- **FORBIDDEN:** `git push origin development`, `git push origin main`, `git push origin master`
- **REQUIRED:** Create feature branch → push feature branch → create PR via MCP
- If you accidentally commit to a protected branch locally: `git checkout -b fix/branch-name` then reset the protected branch
### Repository Rules
| Rule | Details |
|------|---------|
| **File creation** | Only in allowed paths. Use `.scratch/` for temp work. Verify against `docs/CANONICAL-PATHS.md` |
| **plugin.json location** | Must be in `.claude-plugin/` directory |
| **Hooks** | Use `hooks/hooks.json` (auto-discovered). Never inline in plugin.json |
| **MCP servers** | Defined in root `.mcp.json`. Use MCP tools, never CLI (`tea`, `gh`) |
| **Allowed root files** | `CLAUDE.md`, `README.md`, `LICENSE`, `CHANGELOG.md`, `.gitignore`, `.env.example` |
**Valid hook events:** `PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `SessionStart`, `SessionEnd`, `Notification`, `Stop`, `SubagentStop`, `PreCompact`
### ⛔ MANDATORY: Before Any Code Change
**Claude MUST show this checklist BEFORE editing any file:**
#### 1. Impact Search Results
Run and show output of:
```bash
grep -rn "PATTERN" --include="*.sh" --include="*.md" --include="*.json" --include="*.py" | grep -v ".git"
```
#### 2. Files That Will Be Affected
Numbered list of every file to be modified, with the specific change for each.
#### 3. Files Searched But Not Changed (and why)
Proof that related files were checked and determined unchanged.
#### 4. Documentation That References This
List of docs that mention this feature/script/function.
**User verifies this list before Claude proceeds. If Claude skips this, STOP IMMEDIATELY.**
#### After Changes
Run the same grep and show results proving no references remain unaddressed.
---
## ⚠️ Development Context: We Build AND Use These Plugins
**This is a self-referential project.** We are:
1. **BUILDING** a plugin marketplace (source code in `plugins/`)
2. **USING** the installed marketplace to build it (dogfooding)
### Plugins ACTIVELY USED in This Project
These plugins are installed and should be used during development:
| Plugin | Used For |
|--------|----------|
| **projman** | Sprint planning, issue management, lessons learned |
| **git-flow** | Commits, branch management |
| **pr-review** | Pull request reviews |
| **doc-guardian** | Documentation drift detection |
| **code-sentinel** | Security scanning, refactoring |
| **clarity-assist** | Prompt clarification |
| **claude-config-maintainer** | CLAUDE.md optimization |
| **contract-validator** | Cross-plugin compatibility |
### Plugins NOT Used Here (Development Only)
These plugins exist in source but are **NOT relevant** to this project's workflow:
| Plugin | Why Not Used |
|--------|--------------|
| **data-platform** | For data engineering projects (pandas, PostgreSQL, dbt) |
| **viz-platform** | For dashboard projects (Dash, Plotly) |
| **cmdb-assistant** | For infrastructure projects (NetBox) |
**Do NOT suggest** `/data-ingest`, `/data-profile`, `/viz-chart`, `/cmdb-*` commands - they don't apply here.
### Key Distinction
| Context | Path | What To Do |
|---------|------|------------|
| **Editing plugin source** | `~/claude-plugins-work/plugins/` | Modify code, add features |
| **Using installed plugins** | `~/.claude/plugins/marketplaces/` | Run commands like `/sprint-plan` |
When user says "run /sprint-plan", use the INSTALLED plugin.
When user says "fix the sprint-plan command", edit the SOURCE code.
---
## Project Overview
**Repository:** leo-claude-mktplace
**Version:** 7.0.0
**Version:** 4.0.0
**Status:** Production Ready
A plugin marketplace for Claude Code containing:
| Plugin | Description | Version |
|--------|-------------|---------|
| `projman` | Sprint planning and project management with Gitea integration | 3.3.0 |
| `projman` | Sprint planning and project management with Gitea integration | 3.1.0 |
| `git-flow` | Git workflow automation with smart commits and branch management | 1.0.0 |
| `pr-review` | Multi-agent PR review with confidence scoring | 1.1.0 |
| `pr-review` | Multi-agent PR review with confidence scoring | 1.0.0 |
| `clarity-assist` | Prompt optimization with ND-friendly accommodations | 1.0.0 |
| `doc-guardian` | Automatic documentation drift detection and synchronization | 1.0.0 |
| `code-sentinel` | Security scanning and code refactoring tools | 1.0.1 |
| `code-sentinel` | Security scanning and code refactoring tools | 1.0.0 |
| `claude-config-maintainer` | CLAUDE.md optimization and maintenance | 1.0.0 |
| `cmdb-assistant` | NetBox CMDB integration for infrastructure management | 1.2.0 |
| `data-platform` | pandas, PostgreSQL, and dbt integration for data engineering | 1.3.0 |
| `viz-platform` | DMC validation, Plotly charts, and theming for dashboards | 1.1.0 |
| `contract-validator` | Cross-plugin compatibility validation and agent verification | 1.1.0 |
| `cmdb-assistant` | NetBox CMDB integration for infrastructure management | 1.0.0 |
| `data-platform` | pandas, PostgreSQL, and dbt integration for data engineering | 1.0.0 |
| `project-hygiene` | Post-task cleanup automation via hooks | 0.1.0 |
## Quick Start
@@ -173,33 +75,23 @@ A plugin marketplace for Claude Code containing:
./scripts/validate-marketplace.sh
# After updates
./scripts/post-update.sh # Rebuild venvs
./scripts/post-update.sh # Rebuild venvs, verify symlinks
```
### Plugin Commands - USE THESE in This Project
### Plugin Commands by Category
| Category | Commands |
|----------|----------|
| **Setup** | `/pm-setup` (modes: `--full`, `--quick`, `--sync`) |
| **Sprint** | `/sprint-plan`, `/sprint-start`, `/sprint-status` (with `--diagram`), `/sprint-close` |
| **Quality** | `/pm-review`, `/pm-test` (modes: `run`, `gen`) |
| **Setup** | `/initial-setup`, `/project-init`, `/project-sync` |
| **Sprint** | `/sprint-plan`, `/sprint-start`, `/sprint-status`, `/sprint-close` |
| **Quality** | `/review`, `/test-check`, `/test-gen` |
| **Versioning** | `/suggest-version` |
| **PR Review** | `/pr-review`, `/pr-summary`, `/pr-findings`, `/pr-diff` |
| **Docs** | `/doc-audit`, `/doc-sync`, `/changelog-gen`, `/doc-coverage`, `/stale-docs` |
| **PR Review** | `/pr-review:initial-setup`, `/pr-review:project-init` |
| **Docs** | `/doc-audit`, `/doc-sync` |
| **Security** | `/security-scan`, `/refactor`, `/refactor-dry` |
| **Config** | `/config-analyze`, `/config-optimize`, `/config-diff`, `/config-lint` |
| **Validation** | `/validate-contracts`, `/check-agent`, `/list-interfaces`, `/dependency-graph` |
| **Debug** | `/pm-debug` (modes: `report`, `review`) |
### Plugin Commands - NOT RELEVANT to This Project
These commands are being developed but don't apply to this project's workflow:
| Category | Commands | For Projects Using |
|----------|----------|-------------------|
| **Data** | `/data-ingest`, `/data-profile`, `/data-schema`, `/data-lineage`, `/dbt-test` | pandas, PostgreSQL, dbt |
| **Visualization** | `/viz-component`, `/viz-chart`, `/viz-dashboard`, `/viz-theme` | Dash, Plotly dashboards |
| **CMDB** | `/cmdb-search`, `/cmdb-device`, `/cmdb-sync` | NetBox infrastructure |
| **Config** | `/config-analyze`, `/config-optimize` |
| **Data** | `/ingest`, `/profile`, `/schema`, `/explain`, `/lineage`, `/run` |
| **Debug** | `/debug-report`, `/debug-review` |
## Repository Structure
@@ -207,48 +99,44 @@ These commands are being developed but don't apply to this project's workflow:
leo-claude-mktplace/
├── .claude-plugin/
│ └── marketplace.json # Marketplace manifest
├── .mcp.json # MCP server configuration (all servers)
├── mcp-servers/ # SHARED MCP servers
├── mcp-servers/ # SHARED MCP servers (v3.0.0+)
│ ├── gitea/ # Gitea MCP (issues, PRs, wiki)
── netbox/ # NetBox MCP (CMDB)
│ ├── data-platform/ # pandas, PostgreSQL, dbt
│ ├── viz-platform/ # DMC validation, charts, themes
│ └── contract-validator/ # Plugin compatibility validation
── netbox/ # NetBox MCP (CMDB)
├── plugins/
│ ├── projman/ # Sprint management
│ │ ├── .claude-plugin/plugin.json
│ │ ├── commands/ # 12 commands
│ │ ├── hooks/ # SessionStart: mismatch detection
│ │ ├── .mcp.json
│ │ ├── mcp-servers/gitea -> ../../../mcp-servers/gitea # SYMLINK
│ │ ├── commands/ # 14 commands (incl. setup, debug, suggest-version)
│ │ ├── hooks/ # SessionStart: mismatch detection + sprint suggestions
│ │ ├── agents/ # 4 agents
│ │ └── skills/ # 17 reusable skill files
│ │ └── skills/label-taxonomy/
│ ├── git-flow/ # Git workflow automation
│ │ ├── .claude-plugin/plugin.json
│ │ ├── commands/ # 8 commands
│ │ └── agents/
│ ├── pr-review/ # Multi-agent PR review
│ │ ├── .claude-plugin/plugin.json
│ │ ├── commands/ # 6 commands
│ │ ├── .mcp.json
│ │ ├── mcp-servers/gitea -> ../../../mcp-servers/gitea # SYMLINK
│ │ ├── commands/ # 6 commands (incl. setup)
│ │ ├── hooks/ # SessionStart mismatch detection
│ │ └── agents/ # 5 agents
│ ├── clarity-assist/ # Prompt optimization
│ │ ├── .claude-plugin/plugin.json
│ │ ├── commands/ # 2 commands
│ │ └── agents/
│ ├── data-platform/ # Data engineering
│ ├── data-platform/ # Data engineering (NEW v4.0.0)
│ │ ├── .claude-plugin/plugin.json
│ │ ├── .mcp.json
│ │ ├── mcp-servers/ # pandas, postgresql, dbt MCPs
│ │ ├── commands/ # 7 commands
│ │ ├── hooks/ # SessionStart PostgreSQL check
│ │ └── agents/ # 2 agents
│ ├── viz-platform/ # Visualization
│ │ ├── .claude-plugin/plugin.json
│ │ ├── commands/ # 7 commands
│ │ ├── hooks/ # SessionStart DMC check
│ │ └── agents/ # 3 agents
│ ├── doc-guardian/ # Documentation drift detection
│ ├── code-sentinel/ # Security scanning & refactoring
│ ├── claude-config-maintainer/
│ ├── cmdb-assistant/
│ ├── contract-validator/
│ └── project-hygiene/
├── scripts/
│ ├── setup.sh, post-update.sh
@@ -260,6 +148,40 @@ leo-claude-mktplace/
└── CONFIGURATION.md # Centralized configuration guide
```
## CRITICAL: Rules You MUST Follow
### File Operations
- **NEVER** create files in repository root unless listed in "Allowed Root Files"
- **NEVER** modify `.gitignore` without explicit permission
- **ALWAYS** use `.scratch/` for temporary/exploratory work
- **ALWAYS** verify paths against `docs/CANONICAL-PATHS.md` before creating files
### Plugin Development
- **plugin.json MUST be in `.claude-plugin/` directory** (not plugin root)
- **Every plugin MUST be listed in marketplace.json**
- **MCP servers are SHARED at root** with symlinks from plugins
- **MCP server venv path**: `${CLAUDE_PLUGIN_ROOT}/mcp-servers/{name}/.venv/bin/python`
- **CLI tools forbidden** - Use MCP tools exclusively (never `tea`, `gh`, etc.)
#### ⚠️ plugin.json Format Rules (CRITICAL)
- **Hooks in separate file** - Use `hooks/hooks.json` (auto-discovered), NOT inline in plugin.json
- **NEVER reference hooks** - Don't add `"hooks": "..."` field to plugin.json at all
- **Agents auto-discover** - NEVER add `"agents": ["./agents/"]` - .md files found automatically
- **Always validate** - Run `./scripts/validate-marketplace.sh` before committing
- **Working examples:** projman, pr-review, claude-config-maintainer all use `hooks/hooks.json`
- See lesson: `lessons/patterns/plugin-manifest-validation---hooks-and-agents-format-requirements`
### Hooks (Valid Events Only)
`PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `SessionStart`, `SessionEnd`, `Notification`, `Stop`, `SubagentStop`, `PreCompact`
**INVALID:** `task-completed`, `file-changed`, `git-commit-msg-needed`
### Allowed Root Files
`CLAUDE.md`, `README.md`, `LICENSE`, `CHANGELOG.md`, `.gitignore`, `.env.example`
### Allowed Root Directories
`.claude/`, `.claude-plugin/`, `.claude-plugins/`, `.scratch/`, `docs/`, `hooks/`, `mcp-servers/`, `plugins/`, `scripts/`
## Architecture
### Four-Agent Model (projman)
@@ -271,61 +193,6 @@ leo-claude-mktplace/
| **Executor** | Implementation-focused | Code implementation, branch management, MR creation |
| **Code Reviewer** | Thorough, practical | Pre-close quality review, security scan, test verification |
### Agent Frontmatter Configuration
Agents specify their configuration in frontmatter using Claude Code's supported fields. Reference: https://code.claude.com/docs/en/sub-agents
**Supported frontmatter fields:**
| Field | Required | Default | Description |
|-------|----------|---------|-------------|
| `name` | Yes | — | Unique identifier, lowercase + hyphens |
| `description` | Yes | — | When Claude should delegate to this subagent |
| `model` | No | `inherit` | `sonnet`, `opus`, `haiku`, or `inherit` |
| `permissionMode` | No | `default` | Controls permission prompts: `default`, `acceptEdits`, `dontAsk`, `bypassPermissions`, `plan` |
| `disallowedTools` | No | none | Comma-separated tools to remove from agent's toolset |
| `skills` | No | none | Comma-separated skills auto-injected into context at startup |
| `hooks` | No | none | Lifecycle hooks scoped to this subagent |
**Complete agent matrix:**
| Plugin | Agent | `model` | `permissionMode` | `disallowedTools` | `skills` |
|--------|-------|---------|-------------------|--------------------|----------|
| projman | planner | opus | default | — | frontmatter (2) + body text (12) |
| projman | orchestrator | sonnet | acceptEdits | — | frontmatter (2) + body text (10) |
| projman | executor | sonnet | bypassPermissions | — | frontmatter (7) |
| projman | code-reviewer | opus | default | Write, Edit, MultiEdit | frontmatter (4) |
| pr-review | coordinator | sonnet | plan | Write, Edit, MultiEdit | — |
| pr-review | security-reviewer | sonnet | plan | Write, Edit, MultiEdit | — |
| pr-review | performance-analyst | sonnet | plan | Write, Edit, MultiEdit | — |
| pr-review | maintainability-auditor | haiku | plan | Write, Edit, MultiEdit | — |
| pr-review | test-validator | haiku | plan | Write, Edit, MultiEdit | — |
| data-platform | data-advisor | sonnet | default | — | — |
| data-platform | data-analysis | sonnet | plan | Write, Edit, MultiEdit | — |
| data-platform | data-ingestion | haiku | acceptEdits | — | — |
| viz-platform | design-reviewer | sonnet | plan | Write, Edit, MultiEdit | — |
| viz-platform | layout-builder | sonnet | default | — | — |
| viz-platform | component-check | haiku | plan | Write, Edit, MultiEdit | — |
| viz-platform | theme-setup | haiku | acceptEdits | — | — |
| contract-validator | full-validation | sonnet | default | — | — |
| contract-validator | agent-check | haiku | plan | Write, Edit, MultiEdit | — |
| code-sentinel | security-reviewer | sonnet | plan | Write, Edit, MultiEdit | — |
| code-sentinel | refactor-advisor | sonnet | acceptEdits | — | — |
| doc-guardian | doc-analyzer | sonnet | acceptEdits | — | — |
| clarity-assist | clarity-coach | sonnet | default | Write, Edit, MultiEdit | — |
| git-flow | git-assistant | haiku | acceptEdits | — | — |
| claude-config-maintainer | maintainer | sonnet | acceptEdits | — | frontmatter (2) |
| cmdb-assistant | cmdb-assistant | sonnet | default | — | — |
**Design principles:**
- `bypassPermissions` is granted to exactly ONE agent (Executor) which has code-sentinel PreToolUse hook + Code Reviewer downstream as safety nets.
- `plan` mode is assigned to all pure analysis agents (pr-review, read-only validators).
- `disallowedTools: Write, Edit, MultiEdit` provides defense-in-depth on agents that should never write files.
- `skills` frontmatter is used for agents with ≤7 skills where guaranteed loading is safety-critical. Agents with 8+ skills use body text `## Skills to Load` for selective loading.
- `hooks` (agent-scoped) is reserved for future use (v6.0+).
Override any field by editing the agent's `.md` file in `plugins/{plugin}/agents/`.
### MCP Server Tools (Gitea)
| Category | Tools |
@@ -334,7 +201,7 @@ Override any field by editing the agent's `.md` file in `plugins/{plugin}/agents
| Labels | `get_labels`, `suggest_labels`, `create_label`, `create_label_smart` |
| Milestones | `list_milestones`, `get_milestone`, `create_milestone`, `update_milestone`, `delete_milestone` |
| Dependencies | `list_issue_dependencies`, `create_issue_dependency`, `remove_issue_dependency`, `get_execution_order` |
| Wiki | `list_wiki_pages`, `get_wiki_page`, `create_wiki_page`, `update_wiki_page`, `create_lesson`, `search_lessons`, `allocate_rfc_number` |
| Wiki | `list_wiki_pages`, `get_wiki_page`, `create_wiki_page`, `update_wiki_page`, `create_lesson`, `search_lessons` |
| **Pull Requests** | `list_pull_requests`, `get_pull_request`, `get_pr_diff`, `get_pr_comments`, `create_pr_review`, `add_pr_comment` |
| Validation | `validate_repo_org`, `get_branch_protection` |
@@ -355,20 +222,6 @@ Override any field by editing the agent's `.md` file in `plugins/{plugin}/agents
| `staging` | Staging | Read-only code, can create issues |
| `main`, `master` | Production | Read-only, emergency only |
### RFC System
Wiki-based Request for Comments system for tracking feature ideas from proposal through implementation.
**RFC Wiki Naming:**
- RFC pages: `RFC-NNNN: Short Title` (4-digit zero-padded)
- Index page: `RFC-Index` (auto-maintained)
**Lifecycle:** Draft → Review → Approved → Implementing → Implemented
**Integration with Sprint Planning:**
- `/sprint-plan` detects approved RFCs and offers selection
- `/sprint-close` updates RFC status on completion
## Label Taxonomy
43 labels total: 27 organization + 16 repository
@@ -393,15 +246,16 @@ Stored in Gitea Wiki under `lessons-learned/sprints/`.
1. Create `plugins/{name}/.claude-plugin/plugin.json`
2. Add entry to `.claude-plugin/marketplace.json` with category, tags, license
3. Create `claude-md-integration.md`
4. If using new MCP server, add to root `mcp-servers/` and update `.mcp.json`
3. Create `README.md` and `claude-md-integration.md`
4. If using MCP server, create symlink: `ln -s ../../../mcp-servers/{server} plugins/{name}/mcp-servers/{server}`
5. Run `./scripts/validate-marketplace.sh`
6. Update `CHANGELOG.md`
### Adding a Command to projman
1. Create `plugins/projman/commands/{name}.md`
2. Update marketplace description if significant
2. Update `plugins/projman/README.md`
3. Update marketplace description if significant
### Validation
@@ -428,6 +282,7 @@ Stored in Gitea Wiki under `lessons-learned/sprints/`.
| `docs/DEBUGGING-CHECKLIST.md` | Systematic troubleshooting guide |
| `docs/UPDATING.md` | Update guide for the marketplace |
| `plugins/projman/CONFIGURATION.md` | Projman quick reference (links to central) |
| `plugins/projman/README.md` | Projman full documentation |
## Installation Paths
@@ -449,12 +304,12 @@ See `docs/DEBUGGING-CHECKLIST.md` for systematic troubleshooting.
| Symptom | Likely Cause | Fix |
|---------|--------------|-----|
| "X MCP servers failed" | Missing venv in installed path | `cd ~/.claude/plugins/marketplaces/leo-claude-mktplace && ./scripts/setup.sh` |
| MCP tools not available | Venv missing or .mcp.json misconfigured | Run `/pm-debug report` to diagnose |
| MCP tools not available | Symlink broken or venv missing | Run `/debug-report` to diagnose |
| Changes not taking effect | Editing source, not installed | Reinstall plugin or edit installed path |
**Debug Commands:**
- `/pm-debug report` - Run full diagnostics, create issue if needed
- `/pm-debug review` - Investigate and propose fixes
- `/debug-report` - Run full diagnostics, create issue if needed
- `/debug-review` - Investigate and propose fixes
## Versioning Workflow
@@ -508,4 +363,4 @@ The script will:
---
**Last Updated:** 2026-02-03
**Last Updated:** 2026-01-24

127
README.md
View File

@@ -1,4 +1,4 @@
# Leo Claude Marketplace - v7.0.0
# Leo Claude Marketplace - v4.1.0
A collection of Claude Code plugins for project management, infrastructure automation, and development workflows.
@@ -6,13 +6,12 @@ A collection of Claude Code plugins for project management, infrastructure autom
### Development & Project Management
#### [projman](./plugins/projman)
#### [projman](./plugins/projman/README.md)
**Sprint Planning and Project Management**
AI-guided sprint planning with full Gitea integration. Transforms a proven 15-sprint workflow into a distributable plugin.
- Four-agent model: Planner, Orchestrator, Executor, Code Reviewer
- Plan-then-batch execution: skills loaded once per phase, API calls batched for ~80% token savings
- Intelligent label suggestions from 43-label taxonomy
- Lessons learned capture via Gitea Wiki
- Native issue dependencies with parallel execution
@@ -20,9 +19,9 @@ AI-guided sprint planning with full Gitea integration. Transforms a proven 15-sp
- Branch-aware security (development/staging/production)
- Pre-sprint-close code quality review and test verification
**Commands:** `/sprint-plan`, `/sprint-start`, `/sprint-status`, `/sprint-close`, `/labels-sync`, `/pm-setup`, `/pm-review`, `/pm-test`, `/pm-debug`, `/suggest-version`, `/proposal-status`, `/rfc`
**Commands:** `/sprint-plan`, `/sprint-start`, `/sprint-status`, `/sprint-close`, `/labels-sync`, `/initial-setup`, `/project-init`, `/project-sync`, `/review`, `/test-check`, `/test-gen`, `/debug-report`, `/debug-review`
#### [git-flow](./plugins/git-flow) *NEW in v3.0.0*
#### [git-flow](./plugins/git-flow/README.md) *NEW in v3.0.0*
**Git Workflow Automation**
Smart git operations with intelligent commit messages and branch management.
@@ -33,9 +32,9 @@ Smart git operations with intelligent commit messages and branch management.
- Merge and cleanup automation
- Protected branch awareness
**Commands:** `/git-commit`, `/git-commit-push`, `/git-commit-merge`, `/git-commit-sync`, `/branch-start`, `/branch-cleanup`, `/git-status`, `/git-config`
**Commands:** `/commit`, `/commit-push`, `/commit-merge`, `/commit-sync`, `/branch-start`, `/branch-cleanup`, `/git-status`, `/git-config`
#### [pr-review](./plugins/pr-review) *NEW in v3.0.0*
#### [pr-review](./plugins/pr-review/README.md) *NEW in v3.0.0*
**Multi-Agent PR Review**
Comprehensive pull request review using specialized agents.
@@ -45,31 +44,18 @@ Comprehensive pull request review using specialized agents.
- Actionable feedback with suggested fixes
- Gitea integration for automated review submission
**Commands:** `/pr-review`, `/pr-summary`, `/pr-findings`, `/pr-diff`, `/pr-setup`, `/project-init`, `/project-sync`
**Commands:** `/pr-review`, `/pr-summary`, `/pr-findings`, `/initial-setup`, `/project-init`, `/project-sync`
#### [claude-config-maintainer](./plugins/claude-config-maintainer)
**CLAUDE.md and Settings Optimization**
#### [claude-config-maintainer](./plugins/claude-config-maintainer/README.md)
**CLAUDE.md Optimization and Maintenance**
Analyze, optimize, and create CLAUDE.md configuration files. Audit and optimize settings.local.json permissions.
Analyze, optimize, and create CLAUDE.md configuration files for Claude Code projects.
**Commands:** `/analyze`, `/optimize`, `/init`, `/config-diff`, `/config-lint`, `/config-audit-settings`, `/config-optimize-settings`, `/config-permissions-map`
#### [contract-validator](./plugins/contract-validator) *NEW in v5.0.0*
**Cross-Plugin Compatibility Validation**
Validate plugin marketplaces for command conflicts, tool overlaps, and broken agent references.
- Interface parsing from plugin README.md files
- Agent extraction from CLAUDE.md definitions
- Pairwise compatibility checks between all plugins
- Data flow validation for agent sequences
- Markdown or JSON reports with actionable suggestions
**Commands:** `/validate-contracts`, `/check-agent`, `/list-interfaces`, `/dependency-graph`, `/cv-setup`
**Commands:** `/config-analyze`, `/config-optimize`, `/config-init`
### Productivity
#### [clarity-assist](./plugins/clarity-assist) *NEW in v3.0.0*
#### [clarity-assist](./plugins/clarity-assist/README.md) *NEW in v3.0.0*
**Prompt Optimization with ND Accommodations**
Transform vague requests into clear specifications using structured methodology.
@@ -80,21 +66,21 @@ Transform vague requests into clear specifications using structured methodology.
**Commands:** `/clarify`, `/quick-clarify`
#### [doc-guardian](./plugins/doc-guardian)
#### [doc-guardian](./plugins/doc-guardian/README.md)
**Documentation Lifecycle Management**
Automatic documentation drift detection and synchronization.
**Commands:** `/doc-audit`, `/doc-sync`, `/changelog-gen`, `/doc-coverage`, `/stale-docs`
**Commands:** `/doc-audit`, `/doc-sync`
#### [project-hygiene](./plugins/project-hygiene)
#### [project-hygiene](./plugins/project-hygiene/README.md)
**Post-Task Cleanup Automation**
Hook-based cleanup that runs after Claude completes work.
### Security
#### [code-sentinel](./plugins/code-sentinel)
#### [code-sentinel](./plugins/code-sentinel/README.md)
**Security Scanning & Refactoring**
Security vulnerability detection and code refactoring tools.
@@ -103,16 +89,16 @@ Security vulnerability detection and code refactoring tools.
### Infrastructure
#### [cmdb-assistant](./plugins/cmdb-assistant)
#### [cmdb-assistant](./plugins/cmdb-assistant/README.md)
**NetBox CMDB Integration**
Full CRUD operations for network infrastructure management directly from Claude Code.
**Commands:** `/cmdb-setup`, `/cmdb-search`, `/cmdb-device`, `/cmdb-ip`, `/cmdb-site`, `/cmdb-audit`, `/cmdb-register`, `/cmdb-sync`, `/cmdb-topology`, `/change-audit`, `/ip-conflicts`
**Commands:** `/initial-setup`, `/cmdb-search`, `/cmdb-device`, `/cmdb-ip`, `/cmdb-site`
### Data Engineering
#### [data-platform](./plugins/data-platform) *NEW in v4.0.0*
#### [data-platform](./plugins/data-platform/README.md) *NEW*
**pandas, PostgreSQL/PostGIS, and dbt Integration**
Comprehensive data engineering toolkit with persistent DataFrame storage.
@@ -123,42 +109,11 @@ Comprehensive data engineering toolkit with persistent DataFrame storage.
- 100k row limit with chunking support
- Auto-detection of dbt projects
**Commands:** `/data-ingest`, `/data-profile`, `/data-schema`, `/data-explain`, `/data-lineage`, `/lineage-viz`, `/data-run`, `/dbt-test`, `/data-quality`, `/data-review`, `/data-gate`, `/data-setup`
### Visualization
#### [viz-platform](./plugins/viz-platform) *NEW in v4.0.0*
**Dash Mantine Components Validation and Theming**
Visualization toolkit with version-locked component validation and design token theming.
- 3 DMC tools with static JSON registry (prevents prop hallucination)
- 2 Chart tools with Plotly and theme integration
- 5 Layout tools for dashboard composition
- 6 Theme tools with design token system
- 5 Page tools for multi-page app structure
- Dual theme storage: user-level and project-level
**Commands:** `/viz-chart`, `/viz-chart-export`, `/viz-dashboard`, `/viz-theme`, `/viz-theme-new`, `/viz-theme-css`, `/viz-component`, `/accessibility-check`, `/viz-breakpoints`, `/design-review`, `/design-gate`, `/viz-setup`
## Domain Advisory Pattern
The marketplace supports cross-plugin domain advisory integration:
- **Domain Detection**: projman automatically detects when issues involve specialized domains (frontend/viz, data engineering)
- **Acceptance Criteria**: Domain-specific acceptance criteria are added to issues during planning
- **Execution Gates**: Domain validation gates (`/design-gate`, `/data-gate`) run before issue completion
- **Extensible**: New domains can be added by creating advisory agents and gate commands
**Current Domains:**
| Domain | Plugin | Gate Command |
|--------|--------|--------------|
| Visualization | viz-platform | `/design-gate` |
| Data | data-platform | `/data-gate` |
**Commands:** `/ingest`, `/profile`, `/schema`, `/explain`, `/lineage`, `/run`
## MCP Servers
MCP servers are **shared at repository root** and configured in `.mcp.json`.
MCP servers are **shared at repository root** with **symlinks** from plugins that use them.
### Gitea MCP Server (shared)
@@ -186,7 +141,7 @@ Comprehensive NetBox REST API integration for infrastructure management.
| Virtualization | Clusters, VMs, Interfaces |
| Extras | Tags, Custom Fields, Audit Log |
### Data Platform MCP Server (shared) *NEW in v4.0.0*
### Data Platform MCP Server (shared) *NEW*
pandas, PostgreSQL/PostGIS, and dbt integration for data engineering.
@@ -197,28 +152,6 @@ pandas, PostgreSQL/PostGIS, and dbt integration for data engineering.
| PostGIS | `st_tables`, `st_geometry_type`, `st_srid`, `st_extent` |
| dbt | `dbt_parse`, `dbt_run`, `dbt_test`, `dbt_build`, `dbt_compile`, `dbt_ls`, `dbt_docs_generate`, `dbt_lineage` |
### Viz Platform MCP Server (shared) *NEW in v4.0.0*
Dash Mantine Components validation and visualization tools.
| Category | Tools |
|----------|-------|
| DMC | `list_components`, `get_component_props`, `validate_component` |
| Chart | `chart_create`, `chart_configure_interaction` |
| Layout | `layout_create`, `layout_add_filter`, `layout_set_grid`, `layout_get`, `layout_add_section` |
| Theme | `theme_create`, `theme_extend`, `theme_validate`, `theme_export_css`, `theme_list`, `theme_activate` |
| Page | `page_create`, `page_add_navbar`, `page_set_auth`, `page_list`, `page_get_app_config` |
### Contract Validator MCP Server (shared) *NEW in v5.0.0*
Cross-plugin compatibility validation tools.
| Category | Tools |
|----------|-------|
| Parse | `parse_plugin_interface`, `parse_claude_md_agents` |
| Validation | `validate_compatibility`, `validate_agent_refs`, `validate_data_flow`, `validate_workflow_integration` |
| Report | `generate_compatibility_report`, `list_issues` |
## Installation
### Prerequisites
@@ -255,7 +188,7 @@ Add to `.claude/settings.json` in your target project:
After installing plugins, run the setup wizard:
```
/pm-setup
/initial-setup
```
The wizard handles everything:
@@ -313,11 +246,9 @@ After installing plugins, the `/plugin` command may show `(no content)` - this i
| clarity-assist | `/clarity-assist:clarify` |
| doc-guardian | `/doc-guardian:doc-audit` |
| code-sentinel | `/code-sentinel:security-scan` |
| claude-config-maintainer | `/claude-config-maintainer:analyze` |
| claude-config-maintainer | `/claude-config-maintainer:config-analyze` |
| cmdb-assistant | `/cmdb-assistant:cmdb-search` |
| data-platform | `/data-platform:data-ingest` |
| viz-platform | `/viz-platform:viz-chart` |
| contract-validator | `/contract-validator:validate-contracts` |
| data-platform | `/data-platform:ingest` |
## Repository Structure
@@ -328,17 +259,13 @@ leo-claude-mktplace/
├── mcp-servers/ # SHARED MCP servers (v3.0.0+)
│ ├── gitea/ # Gitea MCP (issues, PRs, wiki)
│ ├── netbox/ # NetBox MCP (CMDB)
── data-platform/ # Data engineering (pandas, PostgreSQL, dbt)
│ ├── viz-platform/ # Visualization (DMC, Plotly, theming)
│ └── contract-validator/ # Cross-plugin validation (v5.0.0)
── data-platform/ # Data engineering (pandas, PostgreSQL, dbt)
├── plugins/ # All plugins
│ ├── projman/ # Sprint management
│ ├── git-flow/ # Git workflow automation
│ ├── pr-review/ # PR review
│ ├── clarity-assist/ # Prompt optimization
│ ├── data-platform/ # Data engineering
│ ├── viz-platform/ # Visualization
│ ├── contract-validator/ # Cross-plugin validation (NEW)
│ ├── data-platform/ # Data engineering (NEW)
│ ├── claude-config-maintainer/ # CLAUDE.md optimization
│ ├── cmdb-assistant/ # NetBox CMDB integration
│ ├── doc-guardian/ # Documentation drift detection

View File

@@ -2,7 +2,7 @@
**This file defines ALL valid paths in this repository. No exceptions. No inference. No assumptions.**
Last Updated: 2026-01-30 (v5.4.1)
Last Updated: 2026-01-23 (v3.1.2)
---
@@ -37,45 +37,16 @@ leo-claude-mktplace/
│ │ │ └── pull_requests.py # NEW in v3.0.0
│ │ ├── requirements.txt
│ │ └── .venv/
── netbox/ # NetBox MCP server
│ │ ├── mcp_server/
│ │ ├── requirements.txt
│ │ └── .venv/
│ ├── data-platform/ # Data engineering MCP (NEW v4.0.0)
│ │ ├── mcp_server/
│ │ │ ├── server.py
│ │ │ ├── pandas_tools.py
│ │ │ ├── postgres_tools.py
│ │ │ └── dbt_tools.py
│ │ ├── requirements.txt
│ │ └── .venv/
│ ├── contract-validator/ # Contract validation MCP (NEW v5.0.0)
│ │ ├── mcp_server/
│ │ │ ├── server.py
│ │ │ ├── parse_tools.py
│ │ │ ├── validation_tools.py
│ │ │ └── report_tools.py
│ │ ├── tests/
│ │ ├── requirements.txt
│ │ └── .venv/
│ └── viz-platform/ # Visualization MCP (NEW v4.1.0)
── netbox/ # NetBox MCP server
│ ├── mcp_server/
│ │ ├── server.py
│ │ ├── config.py
│ │ ├── component_registry.py
│ │ ├── dmc_tools.py
│ │ ├── chart_tools.py
│ │ ├── layout_tools.py
│ │ ├── theme_tools.py
│ │ ├── theme_store.py
│ │ └── page_tools.py
│ ├── registry/ # DMC component JSON registries
│ ├── tests/ # 94 tests
│ ├── requirements.txt
│ └── .venv/
├── plugins/ # ALL plugins
│ ├── projman/ # Sprint management
│ │ ├── .claude-plugin/
│ │ ├── .mcp.json
│ │ ├── mcp-servers/
│ │ │ └── gitea -> ../../../mcp-servers/gitea # SYMLINK
│ │ ├── commands/
│ │ ├── agents/
│ │ ├── skills/
@@ -96,6 +67,9 @@ leo-claude-mktplace/
│ │ └── claude-md-integration.md
│ ├── cmdb-assistant/ # NetBox CMDB integration
│ │ ├── .claude-plugin/
│ │ ├── .mcp.json
│ │ ├── mcp-servers/
│ │ │ └── netbox -> ../../../mcp-servers/netbox # SYMLINK
│ │ ├── commands/
│ │ ├── agents/
│ │ └── claude-md-integration.md
@@ -108,49 +82,32 @@ leo-claude-mktplace/
│ │ ├── .claude-plugin/
│ │ ├── hooks/
│ │ └── claude-md-integration.md
│ ├── clarity-assist/
│ ├── clarity-assist/ # NEW in v3.0.0
│ │ ├── .claude-plugin/
│ │ ├── commands/
│ │ ├── agents/
│ │ ├── skills/
│ │ └── claude-md-integration.md
│ ├── git-flow/
│ ├── git-flow/ # NEW in v3.0.0
│ │ ├── .claude-plugin/
│ │ ├── commands/
│ │ ├── agents/
│ │ ├── skills/
│ │ └── claude-md-integration.md
── pr-review/
│ │ ├── .claude-plugin/
│ │ ├── commands/
│ │ ├── agents/
│ │ ├── skills/
│ │ └── claude-md-integration.md
│ ├── data-platform/
│ │ ├── .claude-plugin/
│ │ ├── commands/
│ │ ├── agents/
│ │ ├── hooks/
│ │ └── claude-md-integration.md
│ ├── contract-validator/
│ │ ├── .claude-plugin/
│ │ ├── commands/
│ │ ├── agents/
│ │ └── claude-md-integration.md
│ └── viz-platform/
── pr-review/ # NEW in v3.0.0
│ ├── .claude-plugin/
│ ├── .mcp.json
│ ├── mcp-servers/
│ │ └── gitea -> ../../../mcp-servers/gitea # SYMLINK
│ ├── commands/
│ ├── agents/
│ ├── hooks/
│ ├── skills/
│ └── claude-md-integration.md
├── scripts/ # Setup and maintenance scripts
│ ├── setup.sh # Initial setup (create venvs, config templates)
│ ├── post-update.sh # Post-update (clear cache, show changelog)
│ ├── check-venv.sh # Check if venvs exist (read-only)
── validate-marketplace.sh # Marketplace compliance validation
│ ├── verify-hooks.sh # Verify all hooks use correct event types
│ ├── setup-venvs.sh # Setup MCP server venvs (create only, never delete)
│ └── release.sh # Release automation with version bumping
│ ├── post-update.sh # Post-update (rebuild venvs, verify symlinks)
│ ├── check-venv.sh # Check if venvs exist (for hooks)
── validate-marketplace.sh # Marketplace compliance validation
├── CLAUDE.md
├── README.md
├── LICENSE
@@ -170,53 +127,29 @@ leo-claude-mktplace/
| Plugin manifest | `plugins/{plugin-name}/.claude-plugin/plugin.json` | `plugins/projman/.claude-plugin/plugin.json` |
| Plugin commands | `plugins/{plugin-name}/commands/` | `plugins/projman/commands/` |
| Plugin agents | `plugins/{plugin-name}/agents/` | `plugins/projman/agents/` |
| Plugin skills | `plugins/{plugin-name}/skills/` | `plugins/projman/skills/` |
| Plugin .mcp.json | `plugins/{plugin-name}/.mcp.json` | `plugins/projman/.mcp.json` |
| Plugin integration snippet | `plugins/{plugin-name}/claude-md-integration.md` | `plugins/projman/claude-md-integration.md` |
### MCP Server Paths
### MCP Server Paths (v3.0.0 Architecture)
MCP servers are **shared at repository root** and configured in `.mcp.json`.
MCP servers are **shared at repository root** with **symlinks** from plugins.
| Context | Pattern | Example |
|---------|---------|---------|
| MCP configuration | `.mcp.json` | `.mcp.json` (at repo root) |
| Shared MCP server | `mcp-servers/{server}/` | `mcp-servers/gitea/` |
| MCP server code | `mcp-servers/{server}/mcp_server/` | `mcp-servers/gitea/mcp_server/` |
| MCP venv (local) | `mcp-servers/{server}/.venv/` | `mcp-servers/gitea/.venv/` |
| MCP venv | `mcp-servers/{server}/.venv/` | `mcp-servers/gitea/.venv/` |
| Plugin symlink | `plugins/{plugin}/mcp-servers/{server}` | `plugins/projman/mcp-servers/gitea` |
**Note:** Plugins do NOT have their own `mcp-servers/` directories. All MCP servers are shared at root and configured via `.mcp.json`.
### Symlink Pattern
### MCP Venv Paths - CRITICAL
**Venvs live in a CACHE directory that SURVIVES marketplace updates.**
When checking for venvs, ALWAYS check in this order:
| Priority | Path | Survives Updates? |
|----------|------|-------------------|
| 1 (CHECK FIRST) | `~/.cache/claude-mcp-venvs/leo-claude-mktplace/{server}/.venv/` | YES |
| 2 (fallback) | `{marketplace}/mcp-servers/{server}/.venv/` | NO |
**Why cache first?**
- Marketplace directory gets WIPED on every update/reinstall
- Cache directory SURVIVES updates
- False "venv missing" errors waste hours of debugging
**Pattern for hooks checking venvs:**
Plugins that use MCP servers create symlinks:
```bash
CACHE_VENV="$HOME/.cache/claude-mcp-venvs/leo-claude-mktplace/{server}/.venv/bin/python"
LOCAL_VENV="$MARKETPLACE_ROOT/mcp-servers/{server}/.venv/bin/python"
if [[ -f "$CACHE_VENV" ]]; then
VENV_PATH="$CACHE_VENV"
elif [[ -f "$LOCAL_VENV" ]]; then
VENV_PATH="$LOCAL_VENV"
else
echo "venv missing"
fi
# From plugin directory
ln -s ../../../mcp-servers/gitea plugins/projman/mcp-servers/gitea
```
**See lesson learned:** [Startup Hooks Must Check Venv Cache Path First](https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/wiki/lessons/patterns/startup-hooks-must-check-venv-cache-path-first)
The symlink target is relative: `../../../mcp-servers/{server}`
### Documentation Paths
@@ -245,12 +178,15 @@ fi
2. Verify each path against patterns in this file
3. Show verification to user before proceeding
### Relative Path Calculation
### Relative Path Calculation (v3.0.0)
From `.mcp.json` (at root) to `mcp-servers/gitea/`:
From `plugins/projman/.mcp.json` to shared `mcp-servers/gitea/`:
```
.mcp.json (at repository root)
→ Uses absolute installed path: ~/.claude/plugins/marketplaces/.../mcp-servers/gitea/run.sh
plugins/projman/.mcp.json
→ Uses ${CLAUDE_PLUGIN_ROOT}/mcp-servers/gitea/
→ Symlink at plugins/projman/mcp-servers/gitea points to ../../../mcp-servers/gitea
Result in .mcp.json: ${CLAUDE_PLUGIN_ROOT}/mcp-servers/gitea/.venv/bin/python
```
From `.claude-plugin/marketplace.json` to `plugins/projman/`:
@@ -269,34 +205,27 @@ Result: ./plugins/projman
| Wrong | Why | Correct |
|-------|-----|---------|
| `projman/` at root | Plugins go in `plugins/` | `plugins/projman/` |
| `mcp-servers/` inside plugins | MCP servers are shared at root | Use root `mcp-servers/` |
| Plugin-level `.mcp.json` | MCP config is at root | Use root `.mcp.json` |
| Hardcoding absolute paths in source | Breaks portability | Use relative paths or `${CLAUDE_PLUGIN_ROOT}` |
| Direct path in .mcp.json to root mcp-servers | Use symlink | Symlink at `plugins/{plugin}/mcp-servers/` |
| Creating new mcp-servers inside plugins | Use shared + symlink | Symlink to `mcp-servers/` |
| Hardcoding absolute paths | Breaks portability | Use `${CLAUDE_PLUGIN_ROOT}` |
---
## Architecture Note
## Architecture Note (v3.0.0)
MCP servers are **shared at repository root** and configured in a single `.mcp.json` file.
MCP servers are now **shared at repository root** with **symlinks** from plugins:
**Benefits:**
- Single source of truth for each MCP server
- Updates apply to all plugins automatically
- No duplication - clean plugin structure
- Simple configuration in one place
- Reduced duplication
- Symlinks work with Claude Code caching
**Configuration:**
All MCP servers are defined in `.mcp.json` at repository root:
```json
{
"mcpServers": {
"gitea": { "command": ".../mcp-servers/gitea/run.sh" },
"netbox": { "command": ".../mcp-servers/netbox/run.sh" },
"data-platform": { "command": ".../mcp-servers/data-platform/run.sh" },
"viz-platform": { "command": ".../mcp-servers/viz-platform/run.sh" },
"contract-validator": { "command": ".../mcp-servers/contract-validator/run.sh" }
}
}
**Symlink Pattern:**
```
plugins/projman/mcp-servers/gitea -> ../../../mcp-servers/gitea
plugins/cmdb-assistant/mcp-servers/netbox -> ../../../mcp-servers/netbox
plugins/pr-review/mcp-servers/gitea -> ../../../mcp-servers/gitea
```
---
@@ -305,10 +234,6 @@ All MCP servers are defined in `.mcp.json` at repository root:
| Date | Change | By |
|------|--------|-----|
| 2026-01-30 | v5.5.0: Removed plugin-level mcp-servers symlinks - all MCP config now in root .mcp.json | Claude Code |
| 2026-01-26 | v5.0.0: Added contract-validator plugin and MCP server | Claude Code |
| 2026-01-26 | v4.1.0: Added viz-platform plugin and MCP server | Claude Code |
| 2026-01-25 | v4.0.0: Added data-platform plugin and MCP server | Claude Code |
| 2026-01-20 | v3.0.0: MCP servers moved to root with symlinks | Claude Code |
| 2026-01-20 | v3.0.0: Added clarity-assist, git-flow, pr-review plugins | Claude Code |
| 2026-01-20 | v3.0.0: Added docs/CONFIGURATION.md | Claude Code |

View File

@@ -9,41 +9,39 @@ Quick reference for all commands in the Leo Claude Marketplace.
| Plugin | Command | Auto | Manual | Description |
|--------|---------|:----:|:------:|-------------|
| **projman** | `/sprint-plan` | | X | Start sprint planning with AI-guided architecture analysis and issue creation |
| **projman** | `/sprint-start` | | X | Begin sprint execution with dependency analysis and parallel task coordination (requires approval or `--force`) |
| **projman** | `/sprint-status` | | X | Check current sprint progress (add `--diagram` for Mermaid visualization) |
| **projman** | `/pm-review` | | X | Pre-sprint-close code quality review (debug artifacts, security, error handling) |
| **projman** | `/pm-test` | | X | Run tests (`/pm-test run`) or generate tests (`/pm-test gen <target>`) |
| **projman** | `/sprint-start` | | X | Begin sprint execution with dependency analysis and parallel task coordination |
| **projman** | `/sprint-status` | | X | Check current sprint progress and identify blockers |
| **projman** | `/review` | | X | Pre-sprint-close code quality review (debug artifacts, security, error handling) |
| **projman** | `/test-check` | | X | Run tests and verify coverage before sprint close |
| **projman** | `/sprint-close` | | X | Complete sprint and capture lessons learned to Gitea Wiki |
| **projman** | `/labels-sync` | | X | Synchronize label taxonomy from Gitea |
| **projman** | `/pm-setup` | | X | Auto-detect mode or use `--full`, `--quick`, `--sync`, `--clear-cache` |
| **projman** | *SessionStart hook* | X | | Detects git remote vs .env mismatch, warns to run `/pm-setup --sync` |
| **projman** | `/pm-debug` | | X | Diagnostics (`/pm-debug report`) or investigate (`/pm-debug review`) |
| **projman** | `/initial-setup` | | X | Full setup wizard: MCP server + system config + project config |
| **projman** | `/project-init` | | X | Quick project setup (assumes system config exists) |
| **projman** | `/project-sync` | | X | Sync config with git remote after repo move/rename |
| **projman** | *SessionStart hook* | X | | Detects git remote vs .env mismatch, warns to run /project-sync |
| **projman** | `/test-gen` | | X | Generate comprehensive tests for specified code |
| **projman** | `/debug-report` | | X | Run diagnostics and create structured issue in marketplace |
| **projman** | `/debug-review` | | X | Investigate diagnostic issues and propose fixes with approval gates |
| **projman** | `/suggest-version` | | X | Analyze CHANGELOG and recommend semantic version bump |
| **projman** | `/proposal-status` | | X | View proposal and implementation hierarchy with status |
| **projman** | `/rfc` | | X | RFC lifecycle management (`/rfc create\|list\|review\|approve\|reject`) |
| **git-flow** | `/git-commit` | | X | Create commit with auto-generated conventional message |
| **git-flow** | `/git-commit-push` | | X | Commit and push to remote in one operation |
| **git-flow** | `/git-commit-merge` | | X | Commit current changes, then merge into target branch |
| **git-flow** | `/git-commit-sync` | | X | Full sync: commit, push, and sync with upstream/base branch |
| **git-flow** | `/commit` | | X | Create commit with auto-generated conventional message |
| **git-flow** | `/commit-push` | | X | Commit and push to remote in one operation |
| **git-flow** | `/commit-merge` | | X | Commit current changes, then merge into target branch |
| **git-flow** | `/commit-sync` | | X | Full sync: commit, push, and sync with upstream/base branch |
| **git-flow** | `/branch-start` | | X | Create new feature/fix/chore branch with naming conventions |
| **git-flow** | `/branch-cleanup` | | X | Remove merged branches locally and optionally on remote |
| **git-flow** | `/git-status` | | X | Enhanced git status with recommendations |
| **git-flow** | `/git-config` | | X | Configure git-flow settings for the project |
| **pr-review** | `/pr-setup` | | X | Setup wizard for pr-review (shares Gitea MCP with projman) |
| **pr-review** | `/initial-setup` | | X | Setup wizard for pr-review (shares Gitea MCP with projman) |
| **pr-review** | `/project-init` | | X | Quick project setup for PR reviews |
| **pr-review** | `/project-sync` | | X | Sync config with git remote after repo move/rename |
| **pr-review** | *SessionStart hook* | X | | Detects git remote vs .env mismatch |
| **pr-review** | `/pr-review` | | X | Full multi-agent PR review with confidence scoring |
| **pr-review** | `/pr-summary` | | X | Quick summary of PR changes |
| **pr-review** | `/pr-findings` | | X | List and filter review findings by category/severity |
| **pr-review** | `/pr-diff` | | X | Formatted diff with inline review comments and annotations |
| **clarity-assist** | `/clarify` | | X | Full 4-D prompt optimization with ND accommodations |
| **clarity-assist** | `/quick-clarify` | | X | Rapid single-pass clarification for simple requests |
| **doc-guardian** | `/doc-audit` | | X | Full documentation audit - scans for doc drift |
| **doc-guardian** | `/doc-sync` | | X | Synchronize pending documentation updates |
| **doc-guardian** | `/changelog-gen` | | X | Generate changelog from conventional commits |
| **doc-guardian** | `/doc-coverage` | | X | Documentation coverage metrics by function/class |
| **doc-guardian** | `/stale-docs` | | X | Flag documentation behind code changes |
| **doc-guardian** | *PostToolUse hook* | X | | Silently detects doc drift on Write/Edit |
| **code-sentinel** | `/security-scan` | | X | Full security audit (SQL injection, XSS, secrets, etc.) |
| **code-sentinel** | `/refactor` | | X | Apply refactoring patterns to improve code |
@@ -52,54 +50,20 @@ Quick reference for all commands in the Leo Claude Marketplace.
| **claude-config-maintainer** | `/config-analyze` | | X | Analyze CLAUDE.md for optimization opportunities |
| **claude-config-maintainer** | `/config-optimize` | | X | Optimize CLAUDE.md structure with preview/backup |
| **claude-config-maintainer** | `/config-init` | | X | Initialize new CLAUDE.md for a project |
| **claude-config-maintainer** | `/config-diff` | | X | Track CLAUDE.md changes over time with behavioral impact |
| **claude-config-maintainer** | `/config-lint` | | X | Lint CLAUDE.md for anti-patterns and best practices |
| **claude-config-maintainer** | `/config-audit-settings` | | X | Audit settings.local.json permissions (100-point score) |
| **claude-config-maintainer** | `/config-optimize-settings` | | X | Optimize permissions (profiles, consolidation, dry-run) |
| **claude-config-maintainer** | `/config-permissions-map` | | X | Visual review layer + permission coverage map |
| **cmdb-assistant** | `/cmdb-setup` | | X | Setup wizard for NetBox MCP server |
| **cmdb-assistant** | `/initial-setup` | | X | Setup wizard for NetBox MCP server |
| **cmdb-assistant** | `/cmdb-search` | | X | Search NetBox for devices, IPs, sites |
| **cmdb-assistant** | `/cmdb-device` | | X | Manage network devices (create, view, update, delete) |
| **cmdb-assistant** | `/cmdb-ip` | | X | Manage IP addresses and prefixes |
| **cmdb-assistant** | `/cmdb-site` | | X | Manage sites, locations, racks, and regions |
| **cmdb-assistant** | `/cmdb-audit` | | X | Data quality analysis (VMs, devices, naming, roles) |
| **cmdb-assistant** | `/cmdb-register` | | X | Register current machine into NetBox with running apps |
| **cmdb-assistant** | `/cmdb-sync` | | X | Sync machine state with NetBox (detect drift, update) |
| **cmdb-assistant** | `/cmdb-topology` | | X | Infrastructure topology diagrams (rack, network, site views) |
| **cmdb-assistant** | `/change-audit` | | X | NetBox audit trail queries with filtering |
| **cmdb-assistant** | `/ip-conflicts` | | X | Detect IP conflicts and overlapping prefixes |
| **project-hygiene** | *PostToolUse hook* | X | | Removes temp files, warns about unexpected root files |
| **data-platform** | `/data-ingest` | | X | Load data from CSV, Parquet, JSON into DataFrame |
| **data-platform** | `/data-profile` | | X | Generate data profiling report with statistics |
| **data-platform** | `/data-schema` | | X | Explore database schemas, tables, columns |
| **data-platform** | `/data-explain` | | X | Explain query execution plan |
| **data-platform** | `/data-lineage` | | X | Show dbt model lineage and dependencies |
| **data-platform** | `/data-run` | | X | Run dbt models with validation |
| **data-platform** | `/lineage-viz` | | X | dbt lineage visualization as Mermaid diagrams |
| **data-platform** | `/dbt-test` | | X | Formatted dbt test runner with summary and failure details |
| **data-platform** | `/data-quality` | | X | DataFrame quality checks (nulls, duplicates, types, outliers) |
| **data-platform** | `/data-setup` | | X | Setup wizard for data-platform MCP servers |
| **data-platform** | `/ingest` | | X | Load data from CSV, Parquet, JSON into DataFrame |
| **data-platform** | `/profile` | | X | Generate data profiling report with statistics |
| **data-platform** | `/schema` | | X | Explore database schemas, tables, columns |
| **data-platform** | `/explain` | | X | Explain query execution plan |
| **data-platform** | `/lineage` | | X | Show dbt model lineage and dependencies |
| **data-platform** | `/run` | | X | Run dbt models with validation |
| **data-platform** | `/initial-setup` | | X | Setup wizard for data-platform MCP servers |
| **data-platform** | *SessionStart hook* | X | | Checks PostgreSQL connection (non-blocking warning) |
| **viz-platform** | `/viz-setup` | | X | Setup wizard for viz-platform MCP server |
| **viz-platform** | `/viz-chart` | | X | Create Plotly charts with theme integration |
| **viz-platform** | `/viz-dashboard` | | X | Create dashboard layouts with filters and grids |
| **viz-platform** | `/viz-theme` | | X | Apply existing theme to visualizations |
| **viz-platform** | `/viz-theme-new` | | X | Create new custom theme with design tokens |
| **viz-platform** | `/viz-theme-css` | | X | Export theme as CSS custom properties |
| **viz-platform** | `/viz-component` | | X | Inspect DMC component props and validation |
| **viz-platform** | `/viz-chart-export` | | X | Export charts to PNG, SVG, PDF via kaleido |
| **viz-platform** | `/accessibility-check` | | X | Color blind validation (WCAG contrast ratios) |
| **viz-platform** | `/viz-breakpoints` | | X | Configure responsive layout breakpoints |
| **viz-platform** | `/design-review` | | X | Detailed design system audits |
| **viz-platform** | `/design-gate` | | X | Binary pass/fail design system validation gates |
| **viz-platform** | *SessionStart hook* | X | | Checks DMC version (non-blocking warning) |
| **data-platform** | `/data-review` | | X | Comprehensive data integrity audits |
| **data-platform** | `/data-gate` | | X | Binary pass/fail data integrity gates |
| **contract-validator** | `/validate-contracts` | | X | Full marketplace compatibility validation |
| **contract-validator** | `/check-agent` | | X | Validate single agent definition |
| **contract-validator** | `/list-interfaces` | | X | Show all plugin interfaces |
| **contract-validator** | `/dependency-graph` | | X | Mermaid visualization of plugin dependencies |
| **contract-validator** | `/cv-setup` | | X | Setup wizard for contract-validator MCP |
---
@@ -107,15 +71,13 @@ Quick reference for all commands in the Leo Claude Marketplace.
| Category | Plugins | Primary Use |
|----------|---------|-------------|
| **Setup** | projman, pr-review, cmdb-assistant, data-platform, viz-platform, contract-validator | `/pm-setup`, `/pr-setup`, `/cmdb-setup`, `/data-setup`, `/viz-setup`, `/cv-setup` |
| **Setup** | projman, pr-review, cmdb-assistant, data-platform | `/initial-setup`, `/project-init` |
| **Task Planning** | projman, clarity-assist | Sprint management, requirement clarification |
| **Code Quality** | code-sentinel, pr-review | Security scanning, PR reviews |
| **Documentation** | doc-guardian, claude-config-maintainer | Doc sync, CLAUDE.md maintenance |
| **Git Operations** | git-flow | Commits, branches, workflow automation |
| **Infrastructure** | cmdb-assistant | NetBox CMDB management |
| **Data Engineering** | data-platform | pandas, PostgreSQL, dbt operations |
| **Visualization** | viz-platform | DMC validation, Plotly charts, theming |
| **Validation** | contract-validator | Cross-plugin compatibility checks |
| **Maintenance** | project-hygiene | Automatic cleanup |
---
@@ -130,28 +92,11 @@ Quick reference for all commands in the Leo Claude Marketplace.
| **code-sentinel** | PreToolUse (Write/Edit) | Scans for security issues; blocks critical vulnerabilities |
| **project-hygiene** | PostToolUse (Write/Edit) | Cleans temp files, warns about misplaced files |
| **data-platform** | SessionStart | Checks PostgreSQL connection; non-blocking warning if unavailable |
| **viz-platform** | SessionStart | Checks DMC version; non-blocking warning if mismatch detected |
---
## Dev Workflow Examples
### Example 0: RFC-Driven Feature Development
Full workflow from idea to implementation using RFCs:
```
1. /clarify # Clarify the feature idea
2. /rfc create # Create RFC from clarified spec
... refine RFC content ...
3. /rfc review 0001 # Submit RFC for review
... review discussion ...
4. /rfc approve 0001 # Approve RFC for implementation
5. /sprint-plan # Select approved RFC for sprint
... implement feature ...
6. /sprint-close # Complete sprint, RFC marked Implemented
```
### Example 1: Starting a New Feature Sprint
A typical workflow for planning and executing a feature sprint:
@@ -163,10 +108,10 @@ A typical workflow for planning and executing a feature sprint:
4. /sprint-start # Begin execution with dependency ordering
5. /branch-start feat/... # Create feature branch
... implement features ...
6. /git-commit # Commit with conventional message
7. /sprint-status --diagram # Check progress with visualization
8. /pm-review # Pre-close quality review
9. /pm-test run # Verify test coverage
6. /commit # Commit with conventional message
7. /sprint-status # Check progress mid-sprint
8. /review # Pre-close quality review
9. /test-check # Verify test coverage
10. /sprint-close # Capture lessons learned
```
@@ -178,8 +123,8 @@ Quick daily workflow with git-flow:
1. /git-status # Check current state
2. /branch-start fix/... # Start bugfix branch
... make changes ...
3. /git-commit # Auto-generate commit message
4. /git-commit-push # Push to remote
3. /commit # Auto-generate commit message
4. /commit-push # Push to remote
5. /branch-cleanup # Clean merged branches
```
@@ -213,8 +158,8 @@ Safe refactoring with preview:
1. /refactor-dry # Preview opportunities
2. /security-scan # Baseline security check
3. /refactor # Apply improvements
4. /pm-test run # Verify nothing broke
5. /git-commit # Commit with descriptive message
4. /test-check # Verify nothing broke
5. /commit # Commit with descriptive message
```
### Example 6: Infrastructure Documentation
@@ -233,12 +178,12 @@ Managing infrastructure with CMDB:
Working with data pipelines:
```
1. /data-ingest file.csv # Load data into DataFrame
2. /data-profile # Generate data profiling report
3. /data-schema # Explore database schemas
4. /data-lineage model_name # View dbt model dependencies
5. /data-run model_name # Execute dbt models
6. /data-explain "SELECT ..." # Analyze query execution plan
1. /ingest file.csv # Load data into DataFrame
2. /profile # Generate data profiling report
3. /schema # Explore database schemas
4. /lineage model_name # View dbt model dependencies
5. /run model_name # Execute dbt models
6. /explain "SELECT ..." # Analyze query execution plan
```
### Example 7: First-Time Setup (New Machine)
@@ -246,7 +191,7 @@ Working with data pipelines:
Setting up the marketplace for the first time:
```
1. /pm-setup --full # Full setup: MCP + system config + project
1. /initial-setup # Full setup: MCP + system config + project
# → Follow prompts for Gitea URL, org
# → Add token manually when prompted
# → Confirm repository name
@@ -260,7 +205,7 @@ Setting up the marketplace for the first time:
Adding a new project when system config exists:
```
1. /pm-setup --quick # Quick project setup (auto-detected)
1. /project-init # Quick project setup
# → Confirms detected repo name
# → Creates .env
2. /labels-sync # Sync Gitea labels
@@ -272,8 +217,8 @@ Adding a new project when system config exists:
## Quick Tips
- **Hooks run automatically** - doc-guardian and code-sentinel protect you without manual invocation
- **Use `/git-commit` over `git commit`** - generates better commit messages following conventions
- **Run `/pm-review` before `/sprint-close`** - catches issues before closing the sprint
- **Use `/commit` over `git commit`** - generates better commit messages following conventions
- **Run `/review` before `/sprint-close`** - catches issues before closing the sprint
- **Use `/clarify` for vague requests** - especially helpful for complex requirements
- **`/refactor-dry` is safe** - always preview before applying refactoring changes
@@ -289,11 +234,9 @@ Some plugins require MCP server connectivity:
| pr-review | Gitea | PR operations and reviews |
| cmdb-assistant | NetBox | Infrastructure CMDB |
| data-platform | pandas, PostgreSQL, dbt | DataFrames, database queries, dbt builds |
| viz-platform | viz-platform | DMC validation, charts, layouts, themes, pages |
| contract-validator | contract-validator | Plugin interface parsing, compatibility validation |
Ensure credentials are configured in `~/.config/claude/gitea.env`, `~/.config/claude/netbox.env`, or `~/.config/claude/postgres.env`.
---
*Last Updated: 2026-02-02*
*Last Updated: 2026-01-25*

View File

@@ -9,10 +9,10 @@ Centralized configuration documentation for all plugins and MCP servers in the L
**After installing the marketplace and plugins via Claude Code:**
```
/pm-setup
/initial-setup
```
The interactive wizard auto-detects what's needed and handles everything except manually adding your API tokens.
The interactive wizard handles everything except manually adding your API tokens.
---
@@ -25,8 +25,7 @@ The interactive wizard auto-detects what's needed and handles everything except
└─────────────────────────────────────────────────────────────────────────────┘
/pm-setup --full
(or /pm-setup auto-detects)
/initial-setup
┌──────────────────────────────┼──────────────────────────────┐
▼ ▼ ▼
@@ -79,8 +78,8 @@ The interactive wizard auto-detects what's needed and handles everything except
┌───────────────┴───────────────┐
▼ ▼
/pm-setup --quick /pm-setup
(explicit mode) (auto-detects mode)
/project-init /initial-setup
(direct path) (smart detection)
│ │
│ ┌──────────┴──────────┐
│ ▼ ▼
@@ -109,7 +108,7 @@ The interactive wizard auto-detects what's needed and handles everything except
## What Runs Automatically vs User Interaction
### `/pm-setup --full` - Full Setup
### `/initial-setup` - Full Setup
| Phase | Type | What Happens |
|-------|------|--------------|
@@ -121,7 +120,7 @@ The interactive wizard auto-detects what's needed and handles everything except
| **6. Project Config** | Automated | Creates `.env` file, checks `.gitignore` |
| **7. Validation** | Automated | Tests API connectivity, shows summary |
### `/pm-setup --quick` - Quick Project Setup
### `/project-init` - Quick Project Setup
| Phase | Type | What Happens |
|-------|------|--------------|
@@ -132,25 +131,23 @@ The interactive wizard auto-detects what's needed and handles everything except
---
## One Command, Three Modes
## Three Commands for Different Scenarios
| Mode | When to Use | What It Does |
|------|-------------|--------------|
| `/pm-setup` | Any time | Auto-detects: runs full, quick, or sync as needed |
| `/pm-setup --full` | First time on a machine | Full setup: MCP server + system config + project config |
| `/pm-setup --quick` | Starting a new project | Quick setup: project config only (assumes system is ready) |
| `/pm-setup --sync` | After repo move/rename | Updates .env to match current git remote |
**Auto-detection logic:**
1. No system config → **full** mode
2. System config exists, no project config → **quick** mode
3. Both exist, git remote differs → **sync** mode
4. Both exist, match → already configured, offer to reconfigure
| Command | When to Use | What It Does |
|---------|-------------|--------------|
| `/initial-setup` | First time on a machine | Full setup: MCP server + system config + project config |
| `/project-init` | Starting a new project | Quick setup: project config only (assumes system is ready) |
| `/project-sync` | After repo move/rename | Updates .env to match current git remote |
**Typical workflow:**
1. Install plugin → run `/pm-setup` (auto-runs full mode)
2. Start new project → run `/pm-setup` (auto-runs quick mode)
3. Repository moved? → run `/pm-setup` (auto-runs sync mode)
1. Install plugin → run `/initial-setup` (once per machine)
2. Start new project → run `/project-init` (once per project)
3. Repository moved? → run `/project-sync` (updates config)
**Smart features:**
- `/initial-setup` detects existing system config and offers quick project setup
- All commands validate org/repo via Gitea API before saving (auto-fills if verified)
- SessionStart hook automatically detects git remote vs .env mismatches
---
@@ -174,7 +171,8 @@ This marketplace uses a **hybrid configuration** approach:
│ PROJECT-LEVEL (once per project) │
│ <project-root>/.env │
├─────────────────────────────────────────────────────────────────┤
│ GITEA_REPORepository as owner/repo format
│ GITEA_ORG Organization for this project
│ GITEA_REPO │ Repository name for this project │
│ GIT_WORKFLOW_STYLE │ (optional) Override system default │
│ PR_REVIEW_* │ (optional) PR review settings │
└─────────────────────────────────────────────────────────────────┘
@@ -182,7 +180,7 @@ This marketplace uses a **hybrid configuration** approach:
**Benefits:**
- Single token per service (update once, use everywhere)
- Easy multi-project setup (just run `/pm-setup` in each project)
- Easy multi-project setup (just run `/project-init` in each project)
- Security (tokens never committed to git, never typed into AI chat)
- Project isolation (each project can override defaults)
@@ -190,7 +188,7 @@ This marketplace uses a **hybrid configuration** approach:
## Prerequisites
Before running `/pm-setup`:
Before running `/initial-setup`:
1. **Python 3.10+** installed
```bash
@@ -213,10 +211,10 @@ Before running `/pm-setup`:
Run the setup wizard in Claude Code:
```
/pm-setup
/initial-setup
```
The wizard will guide you through each step interactively and auto-detect the appropriate mode.
The wizard will guide you through each step interactively.
**Note:** After first-time setup, you'll need to restart your Claude Code session for MCP tools to become available.
@@ -264,7 +262,8 @@ In each project root:
```bash
cat > .env << 'EOF'
GITEA_REPO=your-organization/your-repo-name
GITEA_ORG=your-organization
GITEA_REPO=your-repo-name
EOF
```
@@ -308,7 +307,7 @@ GITEA_API_TOKEN=your_gitea_token_here
| `GITEA_API_URL` | Gitea API endpoint (with `/api/v1`) | `https://gitea.example.com/api/v1` |
| `GITEA_API_TOKEN` | Personal access token | `abc123...` |
**Note:** `GITEA_REPO` is configured at the project level in `owner/repo` format since different projects may belong to different organizations.
**Note:** `GITEA_ORG` is configured at the project level (see below) since different projects may belong to different organizations.
**Generating a Gitea Token:**
1. Log into Gitea → **User Icon** → **Settings**
@@ -363,8 +362,9 @@ GIT_CO_AUTHOR=true
Create `.env` in each project root:
```bash
# Required for projman, pr-review (use owner/repo format)
GITEA_REPO=your-organization/your-repo-name
# Required for projman, pr-review
GITEA_ORG=your-organization
GITEA_REPO=your-repo-name
# Optional: Override git-flow defaults
GIT_WORKFLOW_STYLE=pr-required
@@ -377,7 +377,8 @@ PR_REVIEW_AUTO_SUBMIT=false
| Variable | Required | Description |
|----------|----------|-------------|
| `GITEA_REPO` | Yes | Repository in `owner/repo` format (e.g., `my-org/my-repo`) |
| `GITEA_ORG` | Yes | Gitea organization for this project |
| `GITEA_REPO` | Yes | Repository name (must match Gitea exactly) |
| `GIT_WORKFLOW_STYLE` | No | Override system default |
| `PR_REVIEW_*` | No | PR review settings |
@@ -385,20 +386,17 @@ PR_REVIEW_AUTO_SUBMIT=false
## Plugin Configuration Summary
| Plugin | System Config | Project Config | Setup Command |
|--------|---------------|----------------|---------------|
| **projman** | gitea.env | .env (GITEA_REPO=owner/repo) | `/pm-setup` |
| **pr-review** | gitea.env | .env (GITEA_REPO=owner/repo) | `/pr-setup` |
| Plugin | System Config | Project Config | Setup Commands |
|--------|---------------|----------------|----------------|
| **projman** | gitea.env | .env (GITEA_ORG, GITEA_REPO) | `/initial-setup`, `/project-init`, `/project-sync` |
| **pr-review** | gitea.env | .env (GITEA_ORG, GITEA_REPO) | `/initial-setup`, `/project-init`, `/project-sync` |
| **git-flow** | git-flow.env (optional) | .env (optional) | None needed |
| **clarity-assist** | None | None | None needed |
| **cmdb-assistant** | netbox.env | None | `/cmdb-setup` |
| **data-platform** | postgres.env | .env (optional) | `/data-setup` |
| **viz-platform** | None | .env (optional DMC_VERSION) | `/viz-setup` |
| **cmdb-assistant** | netbox.env | None | `/initial-setup` |
| **doc-guardian** | None | None | None needed |
| **code-sentinel** | None | None | None needed |
| **project-hygiene** | None | None | None needed |
| **claude-config-maintainer** | None | None | None needed |
| **contract-validator** | None | None | `/cv-setup` |
---
@@ -406,224 +404,21 @@ PR_REVIEW_AUTO_SUBMIT=false
Once system-level config is set up, adding new projects is simple:
**Option 1: Use `/project-init` (faster)**
```
cd ~/projects/new-project
/pm-setup
/project-init
```
The command auto-detects that system config exists and runs quick project setup.
---
## Installing Plugins to Consumer Projects
The marketplace provides scripts to install plugins into consumer projects. This sets up the MCP server connections and adds CLAUDE.md integration snippets.
### Install a Plugin
```bash
cd /path/to/leo-claude-mktplace
./scripts/install-plugin.sh <plugin-name> <target-project-path>
**Option 2: Use `/initial-setup` (auto-detects)**
```
cd ~/projects/new-project
/initial-setup
# → Detects system config exists
# → Offers "Quick project setup" option
```
**Examples:**
```bash
# Install data-platform to a portfolio project
./scripts/install-plugin.sh data-platform ~/projects/personal-portfolio
# Install multiple plugins
./scripts/install-plugin.sh viz-platform ~/projects/personal-portfolio
./scripts/install-plugin.sh projman ~/projects/personal-portfolio
```
**What it does:**
1. Validates the plugin exists in the marketplace
2. Adds MCP server entry to target's `.mcp.json` (if plugin has MCP server)
3. Appends integration snippet to target's `CLAUDE.md`
4. Reports changes and lists available commands
**After installation:** Restart your Claude Code session for MCP tools to become available.
### Uninstall a Plugin
```bash
./scripts/uninstall-plugin.sh <plugin-name> <target-project-path>
```
Removes the MCP server entry and CLAUDE.md integration section.
### List Installed Plugins
```bash
./scripts/list-installed.sh <target-project-path>
```
Shows which marketplace plugins are installed, partially installed, or available.
**Output example:**
```
✓ Fully Installed:
PLUGIN VERSION DESCRIPTION
------ ------- -----------
data-platform 1.3.0 pandas, PostgreSQL, and dbt integration...
viz-platform 1.1.0 DMC validation, Plotly charts, and theming...
○ Available (not installed):
projman 3.4.0 Sprint planning and project management...
```
### Plugins with MCP Servers
Not all plugins have MCP servers. The install script handles this automatically:
| Plugin | Has MCP Server | Notes |
|--------|---------------|-------|
| data-platform | ✓ | pandas, PostgreSQL, dbt tools |
| viz-platform | ✓ | DMC validation, chart, theme tools |
| contract-validator | ✓ | Plugin compatibility validation |
| cmdb-assistant | ✓ (via netbox) | NetBox CMDB tools |
| projman | ✓ (via gitea) | Issue, wiki, PR tools |
| pr-review | ✓ (via gitea) | PR review tools |
| git-flow | ✗ | Commands only |
| doc-guardian | ✗ | Commands and hooks only |
| code-sentinel | ✗ | Commands and hooks only |
| clarity-assist | ✗ | Commands only |
### Script Requirements
- **jq** must be installed (`sudo apt install jq`)
- Scripts are idempotent (safe to run multiple times)
---
## Agent Frontmatter Configuration
Agents specify their configuration in frontmatter using Claude Code's supported fields. Reference: https://code.claude.com/docs/en/sub-agents
### Supported Frontmatter Fields
| Field | Required | Default | Description |
|-------|----------|---------|-------------|
| `name` | Yes | — | Unique identifier, lowercase + hyphens |
| `description` | Yes | — | When Claude should delegate to this subagent |
| `model` | No | `inherit` | `sonnet`, `opus`, `haiku`, or `inherit` |
| `permissionMode` | No | `default` | Controls permission prompts: `default`, `acceptEdits`, `dontAsk`, `bypassPermissions`, `plan` |
| `disallowedTools` | No | none | Comma-separated tools to remove from agent's toolset |
| `skills` | No | none | Comma-separated skills auto-injected into context at startup |
| `hooks` | No | none | Lifecycle hooks scoped to this subagent |
### Complete Agent Matrix
| Plugin | Agent | `model` | `permissionMode` | `disallowedTools` | `skills` |
|--------|-------|---------|-------------------|--------------------|----------|
| projman | planner | opus | default | — | frontmatter (2) + body text (12) |
| projman | orchestrator | sonnet | acceptEdits | — | frontmatter (2) + body text (10) |
| projman | executor | sonnet | bypassPermissions | — | frontmatter (7) |
| projman | code-reviewer | opus | default | Write, Edit, MultiEdit | frontmatter (4) |
| pr-review | coordinator | sonnet | plan | Write, Edit, MultiEdit | — |
| pr-review | security-reviewer | sonnet | plan | Write, Edit, MultiEdit | — |
| pr-review | performance-analyst | sonnet | plan | Write, Edit, MultiEdit | — |
| pr-review | maintainability-auditor | haiku | plan | Write, Edit, MultiEdit | — |
| pr-review | test-validator | haiku | plan | Write, Edit, MultiEdit | — |
| data-platform | data-advisor | sonnet | default | — | — |
| data-platform | data-analysis | sonnet | plan | Write, Edit, MultiEdit | — |
| data-platform | data-ingestion | haiku | acceptEdits | — | — |
| viz-platform | design-reviewer | sonnet | plan | Write, Edit, MultiEdit | — |
| viz-platform | layout-builder | sonnet | default | — | — |
| viz-platform | component-check | haiku | plan | Write, Edit, MultiEdit | — |
| viz-platform | theme-setup | haiku | acceptEdits | — | — |
| contract-validator | full-validation | sonnet | default | — | — |
| contract-validator | agent-check | haiku | plan | Write, Edit, MultiEdit | — |
| code-sentinel | security-reviewer | sonnet | plan | Write, Edit, MultiEdit | — |
| code-sentinel | refactor-advisor | sonnet | acceptEdits | — | — |
| doc-guardian | doc-analyzer | sonnet | acceptEdits | — | — |
| clarity-assist | clarity-coach | sonnet | default | Write, Edit, MultiEdit | — |
| git-flow | git-assistant | haiku | acceptEdits | — | — |
| claude-config-maintainer | maintainer | sonnet | acceptEdits | — | frontmatter (2) |
| cmdb-assistant | cmdb-assistant | sonnet | default | — | — |
### Design Principles
- `bypassPermissions` is granted to exactly ONE agent (Executor) which has code-sentinel PreToolUse hook + Code Reviewer downstream as safety nets.
- `plan` mode is assigned to all pure analysis agents (pr-review, read-only validators).
- `disallowedTools: Write, Edit, MultiEdit` provides defense-in-depth on agents that should never write files.
- `skills` frontmatter is used for agents with ≤7 skills where guaranteed loading is safety-critical. Agents with 8+ skills use body text `## Skills to Load` for selective loading.
- `hooks` (agent-scoped) is reserved for future use (v6.0+).
Override any field by editing the agent's `.md` file in `plugins/{plugin}/agents/`.
### permissionMode Guide
| Value | Prompts for file ops? | Prompts for Bash? | Prompts for MCP? | Use when |
|-------|-----------------------|-------------------|-------------------|----------|
| `default` | Yes | Yes | No (MCP bypasses permissions) | You want full visibility |
| `acceptEdits` | No | Yes | No | Core job is file read/write, Bash visibility useful |
| `dontAsk` | No | No (most) | No | Even Bash prompts are friction |
| `bypassPermissions` | No | No | No | Agent has downstream safety layers |
| `plan` | N/A (read-only) | N/A (read-only) | No | Pure analysis, no modifications |
### disallowedTools Guide
Use `disallowedTools` to remove specific tools from an agent's toolset. This is a blacklist — the agent inherits all tools from the main thread, then the listed tools are removed.
Prefer `disallowedTools` over `tools` (whitelist) because:
- New MCP servers are automatically available without updating every agent.
- Less configuration to maintain.
- Easier to audit — you only list what's blocked.
Common patterns:
- `disallowedTools: Write, Edit, MultiEdit` — read-only agent, cannot modify files.
- `disallowedTools: Bash` — no shell access (rare, most agents need at least read-only Bash).
### skills Frontmatter Guide
The `skills` field auto-injects skill file contents into the agent's context window at startup. The agent does NOT need to read the files — they are already present.
**When to use frontmatter `skills`:**
- Agent has ≤7 skills.
- Skills are safety-critical (e.g., `branch-security`, `runaway-detection`).
- You need guaranteed loading — no risk of the agent skipping a skill.
**When to keep body text `## Skills to Load`:**
- Agent has 8+ skills (context window cost too high for full injection).
- Skills are situational — not all needed for every invocation.
- Agent benefits from selective loading based on the specific task.
Skill names in frontmatter are resolved relative to the plugin's `skills/` directory. Use the filename without the `.md` extension.
### Phase-Based Skill Loading (Body Text)
For agents with 8+ skills, use **phase-based loading** in the agent body text. This structures skill reads into logical phases, with explicit instructions to read each skill exactly once.
**Pattern:**
```markdown
## Skill Loading Protocol
**Frontmatter skills (auto-injected, always available — DO NOT re-read these):**
- `skill-a` — description
- `skill-b` — description
**Phase 1 skills — read ONCE at session start:**
- skills/validation-skill.md
- skills/safety-skill.md
**Phase 2 skills — read ONCE when entering main work:**
- skills/workflow-skill.md
- skills/domain-skill.md
**CRITICAL: Read each skill file exactly ONCE. Do NOT re-read skill files between MCP API calls.**
```
**Benefits:**
- Frontmatter skills (always needed) are auto-injected — zero file read cost
- Phase skills are read once at the appropriate time — not re-read per API call
- `batch-execution` skill provides protocol for API-heavy phases
- ~76-83% reduction in skill-related token consumption for typical sprints
**Currently applied to:**
- Planner agent: 2 frontmatter + 12 body text (3 phases)
- Orchestrator agent: 2 frontmatter + 10 body text (2 phases)
Both approaches work. Use `/project-init` when you know the system is already configured.
---
@@ -631,12 +426,12 @@ For agents with 8+ skills, use **phase-based loading** in the agent body text. T
### API Validation
When running `/pm-setup`, the command:
When running `/initial-setup`, `/project-init`, or `/project-sync`, the commands:
1. **Detects** organization and repository from git remote URL
2. **Validates** via Gitea API: `GET /api/v1/repos/{org}/{repo}`
3. **Auto-fills** if repository exists and is accessible (no confirmation needed)
4. **Asks for confirmation** only if validation fails (404 or permission error)
1. **Detect** organization and repository from git remote URL
2. **Validate** via Gitea API: `GET /api/v1/repos/{org}/{repo}`
3. **Auto-fill** if repository exists and is accessible (no confirmation needed)
4. **Ask for confirmation** only if validation fails (404 or permission error)
This catches typos and permission issues before saving configuration.
@@ -644,9 +439,9 @@ This catches typos and permission issues before saving configuration.
When you start a Claude Code session, a hook automatically:
1. Reads `GITEA_REPO` (in `owner/repo` format) from `.env`
1. Reads `GITEA_ORG` and `GITEA_REPO` from `.env`
2. Compares with current `git remote get-url origin`
3. **Warns** if mismatch detected: "Repository location mismatch. Run `/pm-setup --sync` to update."
3. **Warns** if mismatch detected: "Repository location mismatch. Run `/project-sync` to update."
This helps when you:
- Move a repository to a different organization
@@ -708,8 +503,9 @@ If you get 401, regenerate your token in Gitea.
# Check venv exists
ls /path/to/mcp-servers/gitea/.venv
# If missing, create venv (do NOT delete existing venvs)
# Reinstall if missing
cd /path/to/mcp-servers/gitea
rm -rf .venv
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
@@ -722,8 +518,7 @@ deactivate
# Check project .env
cat .env
# Verify GITEA_REPO is in owner/repo format and matches Gitea exactly
# Example: GITEA_REPO=my-org/my-repo
# Verify GITEA_REPO matches the Gitea repository name exactly
```
---
@@ -741,7 +536,7 @@ cat .env
3. **Never type tokens into AI chat**
- Always edit config files directly in your editor
- The `/pm-setup` wizard respects this
- The `/initial-setup` wizard respects this
4. **Rotate tokens periodically**
- Every 6-12 months

View File

@@ -2,7 +2,7 @@
**Purpose:** Systematic approach to diagnose and fix plugin loading issues.
Last Updated: 2026-01-28
Last Updated: 2026-01-22
---
@@ -73,19 +73,25 @@ cd $RUNTIME && ./scripts/setup.sh
---
## Step 4: Verify MCP Configuration
## Step 4: Verify Symlink Resolution
Check `.mcp.json` at marketplace root is correctly configured:
Plugins use symlinks to shared MCP servers. Verify they resolve correctly:
```bash
RUNTIME=~/.claude/plugins/marketplaces/leo-claude-mktplace
# Check .mcp.json exists and has valid content
cat $RUNTIME/.mcp.json | jq '.mcpServers | keys'
# Check symlinks exist and resolve
readlink -f $RUNTIME/plugins/projman/mcp-servers/gitea
readlink -f $RUNTIME/plugins/pr-review/mcp-servers/gitea
readlink -f $RUNTIME/plugins/cmdb-assistant/mcp-servers/netbox
# Should list: gitea, netbox, data-platform, viz-platform, contract-validator
# Should resolve to:
# $RUNTIME/mcp-servers/gitea
# $RUNTIME/mcp-servers/netbox
```
**If broken:** Symlinks are relative. If directory structure differs, they'll break.
---
## Step 5: Test MCP Server Startup
@@ -122,7 +128,7 @@ cat ~/.config/claude/netbox.env
# Project-level config (in target project)
cat /path/to/project/.env
# Should contain: GITEA_REPO=owner/repo (e.g., my-org/my-repo)
# Should contain: GITEA_ORG, GITEA_REPO
```
---
@@ -159,8 +165,10 @@ echo -e "\n=== Virtual Environments ==="
[ -f "$RUNTIME/mcp-servers/gitea/.venv/bin/python" ] && echo "Gitea venv: OK" || echo "Gitea venv: MISSING"
[ -f "$RUNTIME/mcp-servers/netbox/.venv/bin/python" ] && echo "NetBox venv: OK" || echo "NetBox venv: MISSING"
echo -e "\n=== MCP Configuration ==="
[ -f "$RUNTIME/.mcp.json" ] && echo ".mcp.json: OK" || echo ".mcp.json: MISSING"
echo -e "\n=== Symlinks ==="
[ -L "$RUNTIME/plugins/projman/mcp-servers/gitea" ] && echo "projman->gitea: OK" || echo "projman->gitea: MISSING"
[ -L "$RUNTIME/plugins/pr-review/mcp-servers/gitea" ] && echo "pr-review->gitea: OK" || echo "pr-review->gitea: MISSING"
[ -L "$RUNTIME/plugins/cmdb-assistant/mcp-servers/netbox" ] && echo "cmdb-assistant->netbox: OK" || echo "cmdb-assistant->netbox: MISSING"
echo -e "\n=== Config Files ==="
[ -f ~/.config/claude/gitea.env ] && echo "gitea.env: OK" || echo "gitea.env: MISSING"
@@ -174,51 +182,10 @@ echo -e "\n=== Config Files ==="
| Issue | Symptom | Fix |
|-------|---------|-----|
| Missing venvs | "X MCP servers failed" | `cd ~/.claude/plugins/marketplaces/leo-claude-mktplace && ./scripts/setup.sh` |
| Missing .mcp.json | MCP tools not available | Check `.mcp.json` exists at marketplace root |
| Broken symlinks | MCP tools not available | Reinstall marketplace or manually recreate symlinks |
| Wrong path edits | Changes don't take effect | Edit installed path or reinstall after source changes |
| Missing credentials | MCP connection errors | Create `~/.config/claude/gitea.env` with API credentials |
| Invalid hook events | Hooks don't fire | Use only valid event names (see Step 7) |
| Gitea issues not closing | Merged to non-default branch | Manually close issues (see below) |
| MCP changes not taking effect | Session caching | Restart Claude Code session (see below) |
### Gitea Auto-Close Behavior
**Issue:** Using `Closes #XX` or `Fixes #XX` in commit/PR messages does NOT auto-close issues when merging to `development`.
**Root Cause:** Gitea only auto-closes issues when merging to the **default branch** (typically `main` or `master`). Merging to `development`, `staging`, or any other branch will NOT trigger auto-close.
**Workaround:**
1. Use the Gitea MCP tool to manually close issues after merging to development:
```
mcp__plugin_projman_gitea__update_issue(issue_number=XX, state="closed")
```
2. Or close issues via the Gitea web UI
3. The auto-close keywords will still work when the changes are eventually merged to `main`
**Recommendation:** Include the `Closes #XX` keywords in commits anyway - they'll work when the final merge to `main` happens.
### MCP Session Restart Requirement
**Issue:** Changes to MCP servers, hooks, or plugin configuration don't take effect immediately.
**Root Cause:** Claude Code loads MCP tools and plugin configuration at session start. These are cached in session memory and not reloaded dynamically.
**What requires a session restart:**
- MCP server code changes (Python files in `mcp-servers/`)
- Changes to `.mcp.json` files
- Changes to `hooks/hooks.json`
- Changes to `plugin.json`
- Adding new MCP tools or modifying tool signatures
**What does NOT require a restart:**
- Command/skill markdown files (`.md`) - these are read on invocation
- Agent markdown files - read when agent is invoked
**Correct workflow after plugin changes:**
1. Make changes to source files
2. Run `./scripts/verify-hooks.sh` to validate
3. Inform user: "Please restart Claude Code for changes to take effect"
4. **Do NOT clear cache mid-session** - see "Cache Clearing" section
---
@@ -279,8 +246,8 @@ Error: Could not find a suitable TLS CA certificate bundle, invalid path:
Use these commands for automated checking:
- `/debug report` - Run full diagnostics, create issue if problems found
- `/debug review` - Investigate existing diagnostic issues and propose fixes
- `/debug-report` - Run full diagnostics, create issue if problems found
- `/debug-review` - Investigate existing diagnostic issues and propose fixes
---

View File

@@ -46,9 +46,9 @@ cd ~/.claude/plugins/marketplaces/leo-claude-mktplace && ./scripts/setup.sh
## After Updating: Re-run Setup if Needed
### When to Re-run Setup
### When to Re-run `/initial-setup`
You typically **don't need** to re-run setup after updates. However, re-run your plugin's setup command (e.g., `/pm-setup`, `/pr-setup`, `/cmdb-setup`) if:
You typically **don't need** to re-run setup after updates. However, re-run if:
- Changelog mentions **new required environment variables**
- Changelog mentions **breaking changes** to configuration
@@ -97,7 +97,7 @@ When updating, review if changes affect the setup workflow:
1. **Check for setup command changes:**
```bash
git diff HEAD~1 plugins/*/commands/*-setup.md
git diff HEAD~1 plugins/*/commands/initial-setup.md
git diff HEAD~1 plugins/*/commands/project-init.md
git diff HEAD~1 plugins/*/commands/project-sync.md
```
@@ -114,7 +114,7 @@ When updating, review if changes affect the setup workflow:
**If setup commands changed:**
- Review what's new (new validation steps, new prompts, etc.)
- Consider re-running your plugin's setup command or `/project-init` to benefit from improvements
- Consider re-running `/initial-setup` or `/project-init` to benefit from improvements
- Existing configurations remain valid unless changelog notes breaking changes
**If hooks changed:**
@@ -132,8 +132,10 @@ When updating, review if changes affect the setup workflow:
### Dependencies fail to install
```bash
# Install missing dependencies (do NOT delete .venv)
# Rebuild virtual environment
cd mcp-servers/gitea
rm -rf .venv
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
deactivate
@@ -142,7 +144,7 @@ deactivate
### Configuration no longer works
1. Check CHANGELOG.md for breaking changes
2. Run your plugin's setup command (e.g., `/pm-setup`) to re-validate and fix configuration
2. Run `/initial-setup` to re-validate and fix configuration
3. Compare your config files with documentation in `docs/CONFIGURATION.md`
### MCP server won't start after update
@@ -162,7 +164,7 @@ If that doesn't work:
ls ~/.claude/plugins/marketplaces/leo-claude-mktplace/mcp-servers/gitea/.venv
ls ~/.claude/plugins/marketplaces/leo-claude-mktplace/mcp-servers/netbox/.venv
```
3. If missing, run setup.sh as shown above.
3. If missing, the symlinks won't resolve. Run setup.sh as shown above.
4. Restart Claude Code session
5. Check logs for specific errors

View File

@@ -1,20 +0,0 @@
2026-01-26T14:36:42 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/contract-validator/mcp_server/parse_tools.py | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-01-26T14:37:38 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/contract-validator/mcp_server/parse_tools.py | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-01-26T14:37:48 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/contract-validator/mcp_server/parse_tools.py | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-01-26T14:38:05 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/contract-validator/mcp_server/parse_tools.py | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-01-26T14:38:55 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/contract-validator/mcp_server/parse_tools.py | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-01-26T14:39:35 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/contract-validator/mcp_server/parse_tools.py | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-01-26T14:40:19 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/contract-validator/mcp_server/parse_tools.py | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-01-26T15:02:30 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/contract-validator/tests/test_parse_tools.py | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-01-26T15:02:37 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/contract-validator/tests/test_parse_tools.py | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-01-26T15:03:41 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/contract-validator/tests/test_report_tools.py | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-02-02T10:56:19 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/contract-validator/mcp_server/validation_tools.py | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-02-02T10:57:49 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/contract-validator/tests/test_validation_tools.py | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-02-02T10:58:22 | skills | /home/lmiranda/claude-plugins-work/plugins/contract-validator/skills/mcp-tools-reference.md | README.md
2026-02-02T10:58:38 | skills | /home/lmiranda/claude-plugins-work/plugins/contract-validator/skills/validation-rules.md | README.md
2026-02-02T10:59:13 | .claude-plugin | /home/lmiranda/claude-plugins-work/.claude-plugin/marketplace.json | CLAUDE.md .claude-plugin/marketplace.json
2026-02-02T13:55:33 | skills | /home/lmiranda/claude-plugins-work/plugins/projman/skills/visual-output.md | README.md
2026-02-02T13:55:41 | agents | /home/lmiranda/claude-plugins-work/plugins/projman/agents/planner.md | README.md CLAUDE.md
2026-02-02T13:55:55 | agents | /home/lmiranda/claude-plugins-work/plugins/projman/agents/orchestrator.md | README.md CLAUDE.md
2026-02-02T13:56:14 | agents | /home/lmiranda/claude-plugins-work/plugins/projman/agents/executor.md | README.md CLAUDE.md
2026-02-02T13:56:34 | agents | /home/lmiranda/claude-plugins-work/plugins/projman/agents/code-reviewer.md | README.md CLAUDE.md

View File

@@ -1,3 +0,0 @@
"""Contract Validator MCP Server - Cross-plugin compatibility validation."""
__version__ = "1.0.0"

View File

@@ -1,415 +0,0 @@
"""
Parse tools for extracting interfaces from plugin documentation.
Provides structured extraction of:
- Plugin interfaces from README.md (commands, agents, tools)
- Agent definitions from CLAUDE.md (tool sequences, workflows)
"""
import re
import os
from pathlib import Path
from typing import Optional
from pydantic import BaseModel
class ToolInfo(BaseModel):
"""Information about a single tool"""
name: str
category: Optional[str] = None
description: Optional[str] = None
class CommandInfo(BaseModel):
"""Information about a plugin command"""
name: str
description: Optional[str] = None
class AgentInfo(BaseModel):
"""Information about a plugin agent"""
name: str
description: Optional[str] = None
tools: list[str] = []
class PluginInterface(BaseModel):
"""Structured plugin interface extracted from README"""
plugin_name: str
description: Optional[str] = None
commands: list[CommandInfo] = []
agents: list[AgentInfo] = []
tools: list[ToolInfo] = []
tool_categories: dict[str, list[str]] = {}
features: list[str] = []
class ClaudeMdAgent(BaseModel):
"""Agent definition extracted from CLAUDE.md"""
name: str
personality: Optional[str] = None
responsibilities: list[str] = []
tool_refs: list[str] = []
workflow_steps: list[str] = []
class ParseTools:
"""Tools for parsing plugin documentation"""
async def parse_plugin_interface(self, plugin_path: str) -> dict:
"""
Parse plugin README.md to extract interface declarations.
Args:
plugin_path: Path to plugin directory or README.md file
Returns:
Structured interface with commands, agents, tools, etc.
"""
# Resolve path to README
path = Path(plugin_path)
if path.is_dir():
readme_path = path / "README.md"
else:
readme_path = path
if not readme_path.exists():
return {
"error": f"README.md not found at {readme_path}",
"plugin_path": plugin_path
}
content = readme_path.read_text()
plugin_name = self._extract_plugin_name(content, path)
interface = PluginInterface(
plugin_name=plugin_name,
description=self._extract_description(content),
commands=self._extract_commands(content),
agents=self._extract_agents_from_readme(content),
tools=self._extract_tools(content),
tool_categories=self._extract_tool_categories(content),
features=self._extract_features(content)
)
return interface.model_dump()
async def parse_claude_md_agents(self, claude_md_path: str) -> dict:
"""
Parse CLAUDE.md to extract agent definitions and tool sequences.
Args:
claude_md_path: Path to CLAUDE.md file
Returns:
List of agents with their tool sequences
"""
path = Path(claude_md_path)
if not path.exists():
return {
"error": f"CLAUDE.md not found at {path}",
"claude_md_path": claude_md_path
}
content = path.read_text()
agents = self._extract_agents_from_claude_md(content)
return {
"file": str(path),
"agents": [a.model_dump() for a in agents],
"agent_count": len(agents)
}
def _extract_plugin_name(self, content: str, path: Path) -> str:
"""Extract plugin name from content or path"""
# Try to get from H1 header
match = re.search(r'^#\s+(.+?)(?:\s+Plugin|\s*$)', content, re.MULTILINE)
if match:
name = match.group(1).strip()
# Handle cases like "# data-platform Plugin"
name = re.sub(r'\s*Plugin\s*$', '', name, flags=re.IGNORECASE)
return name
# Fall back to directory name
if path.is_dir():
return path.name
return path.parent.name
def _extract_description(self, content: str) -> Optional[str]:
"""Extract plugin description from first paragraph after title"""
# Get content after H1, before first H2
match = re.search(r'^#\s+.+?\n\n(.+?)(?=\n##|\n\n##|\Z)', content, re.MULTILINE | re.DOTALL)
if match:
desc = match.group(1).strip()
# Take first paragraph only
desc = desc.split('\n\n')[0].strip()
return desc
return None
def _extract_commands(self, content: str) -> list[CommandInfo]:
"""Extract commands from Commands section"""
commands = []
# Find Commands section
commands_section = self._extract_section(content, "Commands")
if not commands_section:
return commands
# Parse table format: | Command | Description |
# Only match actual command names (start with / or alphanumeric)
table_pattern = r'\|\s*`?(/[a-z][-a-z0-9]*)`?\s*\|\s*([^|]+)\s*\|'
for match in re.finditer(table_pattern, commands_section):
cmd_name = match.group(1).strip()
desc = match.group(2).strip()
# Skip header row and separators
if cmd_name.lower() in ('command', 'commands') or cmd_name.startswith('-'):
continue
commands.append(CommandInfo(
name=cmd_name,
description=desc
))
# Also look for ### `/command-name` format (with backticks)
cmd_header_pattern = r'^###\s+`(/[a-z][-a-z0-9]*)`\s*\n(.+?)(?=\n###|\n##|\Z)'
for match in re.finditer(cmd_header_pattern, commands_section, re.MULTILINE | re.DOTALL):
cmd_name = match.group(1).strip()
desc_block = match.group(2).strip()
# Get first line or paragraph as description
desc = desc_block.split('\n')[0].strip()
# Don't duplicate if already found in table
if not any(c.name == cmd_name for c in commands):
commands.append(CommandInfo(name=cmd_name, description=desc))
# Also look for ### /command-name format (without backticks)
cmd_header_pattern2 = r'^###\s+(/[a-z][-a-z0-9]*)\s*\n(.+?)(?=\n###|\n##|\Z)'
for match in re.finditer(cmd_header_pattern2, commands_section, re.MULTILINE | re.DOTALL):
cmd_name = match.group(1).strip()
desc_block = match.group(2).strip()
# Get first line or paragraph as description
desc = desc_block.split('\n')[0].strip()
# Don't duplicate if already found in table
if not any(c.name == cmd_name for c in commands):
commands.append(CommandInfo(name=cmd_name, description=desc))
return commands
def _extract_agents_from_readme(self, content: str) -> list[AgentInfo]:
"""Extract agents from Agents section in README"""
agents = []
# Find Agents section
agents_section = self._extract_section(content, "Agents")
if not agents_section:
return agents
# Parse table format: | Agent | Description |
# Only match actual agent names (alphanumeric with dashes/underscores)
table_pattern = r'\|\s*`?([a-z][-a-z0-9_]*)`?\s*\|\s*([^|]+)\s*\|'
for match in re.finditer(table_pattern, agents_section):
agent_name = match.group(1).strip()
desc = match.group(2).strip()
# Skip header row and separators
if agent_name.lower() in ('agent', 'agents') or agent_name.startswith('-'):
continue
agents.append(AgentInfo(name=agent_name, description=desc))
return agents
def _extract_tools(self, content: str) -> list[ToolInfo]:
"""Extract tool list from Tools Summary or similar section"""
tools = []
# Find Tools Summary section
tools_section = self._extract_section(content, "Tools Summary")
if not tools_section:
tools_section = self._extract_section(content, "Tools")
if not tools_section:
tools_section = self._extract_section(content, "MCP Server Tools")
if not tools_section:
return tools
# Parse category headers: ### category (N tools)
category_pattern = r'###\s*(.+?)\s*(?:\((\d+)\s*tools?\))?\s*\n([^#]+)'
for match in re.finditer(category_pattern, tools_section):
category = match.group(1).strip()
tool_list_text = match.group(3).strip()
# Extract tool names from backtick lists
tool_names = re.findall(r'`([a-z_]+)`', tool_list_text)
for name in tool_names:
tools.append(ToolInfo(name=name, category=category))
# Also look for inline tool lists without categories
inline_pattern = r'`([a-z_]+)`'
all_tool_names = set(t.name for t in tools)
for match in re.finditer(inline_pattern, tools_section):
name = match.group(1)
if name not in all_tool_names:
tools.append(ToolInfo(name=name))
all_tool_names.add(name)
return tools
def _extract_tool_categories(self, content: str) -> dict[str, list[str]]:
"""Extract tool categories with their tool lists"""
categories = {}
tools_section = self._extract_section(content, "Tools Summary")
if not tools_section:
tools_section = self._extract_section(content, "Tools")
if not tools_section:
return categories
# Parse category headers: ### category (N tools)
category_pattern = r'###\s*(.+?)\s*(?:\((\d+)\s*tools?\))?\s*\n([^#]+)'
for match in re.finditer(category_pattern, tools_section):
category = match.group(1).strip()
tool_list_text = match.group(3).strip()
# Extract tool names from backtick lists
tool_names = re.findall(r'`([a-z_]+)`', tool_list_text)
if tool_names:
categories[category] = tool_names
return categories
def _extract_features(self, content: str) -> list[str]:
"""Extract features from Features section"""
features = []
features_section = self._extract_section(content, "Features")
if not features_section:
return features
# Parse bullet points
bullet_pattern = r'^[-*]\s+\*\*(.+?)\*\*'
for match in re.finditer(bullet_pattern, features_section, re.MULTILINE):
features.append(match.group(1).strip())
return features
def _extract_section(self, content: str, section_name: str) -> Optional[str]:
"""Extract content of a markdown section by header name"""
# Match ## Section Name - include all content until next ## (same level or higher)
pattern = rf'^##\s+{re.escape(section_name)}(?:\s*\([^)]*\))?\s*\n(.*?)(?=\n##[^#]|\Z)'
match = re.search(pattern, content, re.MULTILINE | re.DOTALL | re.IGNORECASE)
if match:
return match.group(1).strip()
# Try ### level - include content until next ## or ###
pattern = rf'^###\s+{re.escape(section_name)}(?:\s*\([^)]*\))?\s*\n(.*?)(?=\n##|\n###[^#]|\Z)'
match = re.search(pattern, content, re.MULTILINE | re.DOTALL | re.IGNORECASE)
if match:
return match.group(1).strip()
return None
def _extract_agents_from_claude_md(self, content: str) -> list[ClaudeMdAgent]:
"""Extract agent definitions from CLAUDE.md"""
agents = []
# Look for Four-Agent Model section specifically
# Match section headers like "### Four-Agent Model (projman)" or "## Four-Agent Model"
agent_model_match = re.search(
r'^##[#]?\s+Four-Agent Model.*?\n(.*?)(?=\n##[^#]|\Z)',
content, re.MULTILINE | re.DOTALL
)
agent_model_section = agent_model_match.group(1) if agent_model_match else None
if agent_model_section:
# Parse agent table within this section
# | **Planner** | Thoughtful, methodical | Sprint planning, ... |
# Match rows where first cell starts with ** (bold) and contains a capitalized word
agent_table_pattern = r'\|\s*\*\*([A-Z][a-zA-Z\s]+?)\*\*\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|'
for match in re.finditer(agent_table_pattern, agent_model_section):
agent_name = match.group(1).strip()
personality = match.group(2).strip()
responsibilities = match.group(3).strip()
# Skip header rows and separator rows
if agent_name.lower() in ('agent', 'agents', '---', '-', ''):
continue
if 'personality' in personality.lower() or '---' in personality:
continue
# Skip if personality looks like tool names (contains backticks)
if '`' in personality:
continue
# Extract tool references from responsibilities
tool_refs = re.findall(r'`([a-z_]+)`', responsibilities)
# Split responsibilities by comma
resp_list = [r.strip() for r in responsibilities.split(',')]
agents.append(ClaudeMdAgent(
name=agent_name,
personality=personality,
responsibilities=resp_list,
tool_refs=tool_refs
))
# Also look for agents table in ## Agents section
agents_section = self._extract_section(content, "Agents")
if agents_section:
# Parse table: | Agent | Description |
table_pattern = r'\|\s*`?([a-z][-a-z0-9_]+)`?\s*\|\s*([^|]+)\s*\|'
for match in re.finditer(table_pattern, agents_section):
agent_name = match.group(1).strip()
desc = match.group(2).strip()
# Skip header rows
if agent_name.lower() in ('agent', 'agents', '---', '-'):
continue
# Check if agent already exists
if not any(a.name.lower() == agent_name.lower() for a in agents):
agents.append(ClaudeMdAgent(
name=agent_name,
responsibilities=[desc] if desc else []
))
# Look for workflow sections to enrich agent data
workflow_section = self._extract_section(content, "Workflow")
if workflow_section:
# Parse numbered steps
step_pattern = r'^\d+\.\s+(.+?)$'
workflow_steps = re.findall(step_pattern, workflow_section, re.MULTILINE)
# Associate workflow steps with agents mentioned
for agent in agents:
for step in workflow_steps:
if agent.name.lower() in step.lower():
agent.workflow_steps.append(step)
# Extract any tool references in the step
step_tools = re.findall(r'`([a-z_]+)`', step)
agent.tool_refs.extend(t for t in step_tools if t not in agent.tool_refs)
# Look for agent-specific sections (### Planner Agent)
agent_section_pattern = r'^###?\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)\s+Agent\s*\n(.*?)(?=\n##|\n###|\Z)'
for match in re.finditer(agent_section_pattern, content, re.MULTILINE | re.DOTALL):
agent_name = match.group(1).strip()
section_content = match.group(2).strip()
# Check if agent already exists
existing = next((a for a in agents if a.name.lower() == agent_name.lower()), None)
if existing:
# Add tool refs from this section
tool_refs = re.findall(r'`([a-z_]+)`', section_content)
existing.tool_refs.extend(t for t in tool_refs if t not in existing.tool_refs)
else:
tool_refs = re.findall(r'`([a-z_]+)`', section_content)
agents.append(ClaudeMdAgent(
name=agent_name,
tool_refs=tool_refs
))
return agents

View File

@@ -1,337 +0,0 @@
"""
Report tools for generating compatibility reports and listing issues.
Provides:
- generate_compatibility_report: Full marketplace validation report
- list_issues: Filtered issue listing
"""
import os
from pathlib import Path
from datetime import datetime
from typing import Optional
from pydantic import BaseModel
from .parse_tools import ParseTools
from .validation_tools import ValidationTools, IssueSeverity, IssueType, ValidationIssue
class ReportSummary(BaseModel):
"""Summary statistics for a report"""
total_plugins: int = 0
total_commands: int = 0
total_agents: int = 0
total_tools: int = 0
total_issues: int = 0
errors: int = 0
warnings: int = 0
info: int = 0
class ReportTools:
"""Tools for generating reports and listing issues"""
def __init__(self):
self.parse_tools = ParseTools()
self.validation_tools = ValidationTools()
async def generate_compatibility_report(
self,
marketplace_path: str,
format: str = "markdown"
) -> dict:
"""
Generate a comprehensive compatibility report for all plugins.
Args:
marketplace_path: Path to marketplace root directory
format: Output format ("markdown" or "json")
Returns:
Full compatibility report with all findings
"""
marketplace = Path(marketplace_path)
plugins_dir = marketplace / "plugins"
if not plugins_dir.exists():
return {
"error": f"Plugins directory not found at {plugins_dir}",
"marketplace_path": marketplace_path
}
# Discover all plugins
plugins = []
for item in plugins_dir.iterdir():
if item.is_dir() and (item / ".claude-plugin").exists():
plugins.append(item)
if not plugins:
return {
"error": "No plugins found in marketplace",
"marketplace_path": marketplace_path
}
# Parse all plugin interfaces
interfaces = {}
all_issues = []
summary = ReportSummary(total_plugins=len(plugins))
for plugin_path in plugins:
interface = await self.parse_tools.parse_plugin_interface(str(plugin_path))
if "error" not in interface:
interfaces[interface["plugin_name"]] = interface
summary.total_commands += len(interface.get("commands", []))
summary.total_agents += len(interface.get("agents", []))
summary.total_tools += len(interface.get("tools", []))
# Run pairwise compatibility checks
plugin_names = list(interfaces.keys())
compatibility_results = []
for i, name_a in enumerate(plugin_names):
for name_b in plugin_names[i+1:]:
path_a = plugins_dir / self._find_plugin_dir(plugins_dir, name_a)
path_b = plugins_dir / self._find_plugin_dir(plugins_dir, name_b)
result = await self.validation_tools.validate_compatibility(
str(path_a), str(path_b)
)
if "error" not in result:
compatibility_results.append(result)
all_issues.extend(result.get("issues", []))
# Parse CLAUDE.md if exists
claude_md = marketplace / "CLAUDE.md"
agents_from_claude = []
if claude_md.exists():
agents_result = await self.parse_tools.parse_claude_md_agents(str(claude_md))
if "error" not in agents_result:
agents_from_claude = agents_result.get("agents", [])
# Validate each agent
for agent in agents_from_claude:
agent_result = await self.validation_tools.validate_agent_refs(
agent["name"],
str(claude_md),
[str(p) for p in plugins]
)
if "error" not in agent_result:
all_issues.extend(agent_result.get("issues", []))
# Count issues by severity
for issue in all_issues:
severity = issue.get("severity", "info")
if isinstance(severity, str):
severity_str = severity.lower()
else:
severity_str = severity.value if hasattr(severity, 'value') else str(severity).lower()
if "error" in severity_str:
summary.errors += 1
elif "warning" in severity_str:
summary.warnings += 1
else:
summary.info += 1
summary.total_issues = len(all_issues)
# Generate report
if format == "json":
return {
"generated_at": datetime.now().isoformat(),
"marketplace_path": marketplace_path,
"summary": summary.model_dump(),
"plugins": interfaces,
"compatibility_checks": compatibility_results,
"claude_md_agents": agents_from_claude,
"all_issues": all_issues
}
else:
# Generate markdown report
report = self._generate_markdown_report(
marketplace_path,
summary,
interfaces,
compatibility_results,
agents_from_claude,
all_issues
)
return {
"generated_at": datetime.now().isoformat(),
"marketplace_path": marketplace_path,
"summary": summary.model_dump(),
"report": report
}
def _find_plugin_dir(self, plugins_dir: Path, plugin_name: str) -> str:
"""Find plugin directory by name (handles naming variations)"""
# Try exact match first
for item in plugins_dir.iterdir():
if item.is_dir():
if item.name.lower() == plugin_name.lower():
return item.name
# Check plugin.json for name
plugin_json = item / ".claude-plugin" / "plugin.json"
if plugin_json.exists():
import json
try:
data = json.loads(plugin_json.read_text())
if data.get("name", "").lower() == plugin_name.lower():
return item.name
except:
pass
return plugin_name
def _generate_markdown_report(
self,
marketplace_path: str,
summary: ReportSummary,
interfaces: dict,
compatibility_results: list,
agents: list,
issues: list
) -> str:
"""Generate markdown formatted report"""
lines = [
"# Contract Validation Report",
"",
f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
f"**Marketplace:** `{marketplace_path}`",
"",
"## Summary",
"",
f"| Metric | Count |",
f"|--------|-------|",
f"| Plugins | {summary.total_plugins} |",
f"| Commands | {summary.total_commands} |",
f"| Agents | {summary.total_agents} |",
f"| Tools | {summary.total_tools} |",
f"| **Issues** | **{summary.total_issues}** |",
f"| - Errors | {summary.errors} |",
f"| - Warnings | {summary.warnings} |",
f"| - Info | {summary.info} |",
"",
]
# Plugin details
lines.extend([
"## Plugins",
"",
])
for name, interface in interfaces.items():
cmds = len(interface.get("commands", []))
agents_count = len(interface.get("agents", []))
tools = len(interface.get("tools", []))
lines.append(f"### {name}")
lines.append("")
lines.append(f"- Commands: {cmds}")
lines.append(f"- Agents: {agents_count}")
lines.append(f"- Tools: {tools}")
lines.append("")
# Compatibility results
if compatibility_results:
lines.extend([
"## Compatibility Checks",
"",
])
for result in compatibility_results:
status = "" if result.get("compatible", True) else ""
lines.append(f"### {result['plugin_a']}{result['plugin_b']} {status}")
lines.append("")
if result.get("shared_tools"):
lines.append(f"- Shared tools: `{', '.join(result['shared_tools'])}`")
if result.get("issues"):
for issue in result["issues"]:
sev = issue.get("severity", "info")
if hasattr(sev, 'value'):
sev = sev.value
lines.append(f"- [{sev.upper()}] {issue['message']}")
lines.append("")
# Issues section
if issues:
lines.extend([
"## All Issues",
"",
"| Severity | Type | Message |",
"|----------|------|---------|",
])
for issue in issues:
sev = issue.get("severity", "info")
itype = issue.get("issue_type", "unknown")
msg = issue.get("message", "")
if hasattr(sev, 'value'):
sev = sev.value
if hasattr(itype, 'value'):
itype = itype.value
# Truncate message for table
msg_short = msg[:60] + "..." if len(msg) > 60 else msg
lines.append(f"| {sev} | {itype} | {msg_short} |")
lines.append("")
return "\n".join(lines)
async def list_issues(
self,
marketplace_path: str,
severity: str = "all",
issue_type: str = "all"
) -> dict:
"""
List validation issues with optional filtering.
Args:
marketplace_path: Path to marketplace root directory
severity: Filter by severity ("error", "warning", "info", "all")
issue_type: Filter by type ("missing_tool", "interface_mismatch", etc., "all")
Returns:
Filtered list of issues
"""
# Generate full report first
report = await self.generate_compatibility_report(marketplace_path, format="json")
if "error" in report:
return report
all_issues = report.get("all_issues", [])
# Filter by severity
if severity != "all":
filtered = []
for issue in all_issues:
issue_sev = issue.get("severity", "info")
if hasattr(issue_sev, 'value'):
issue_sev = issue_sev.value
if isinstance(issue_sev, str) and severity.lower() in issue_sev.lower():
filtered.append(issue)
all_issues = filtered
# Filter by type
if issue_type != "all":
filtered = []
for issue in all_issues:
itype = issue.get("issue_type", "unknown")
if hasattr(itype, 'value'):
itype = itype.value
if isinstance(itype, str) and issue_type.lower() in itype.lower():
filtered.append(issue)
all_issues = filtered
return {
"marketplace_path": marketplace_path,
"filters": {
"severity": severity,
"issue_type": issue_type
},
"total_issues": len(all_issues),
"issues": all_issues
}

View File

@@ -1,309 +0,0 @@
"""
MCP Server entry point for Contract Validator.
Provides cross-plugin compatibility validation and Claude.md agent verification
tools to Claude Code via JSON-RPC 2.0 over stdio.
"""
import asyncio
import logging
import json
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
from .parse_tools import ParseTools
from .validation_tools import ValidationTools
from .report_tools import ReportTools
# Suppress noisy MCP validation warnings on stderr
logging.basicConfig(level=logging.INFO)
logging.getLogger("root").setLevel(logging.ERROR)
logging.getLogger("mcp").setLevel(logging.ERROR)
logger = logging.getLogger(__name__)
class ContractValidatorMCPServer:
"""MCP Server for cross-plugin compatibility validation"""
def __init__(self):
self.server = Server("contract-validator-mcp")
self.parse_tools = ParseTools()
self.validation_tools = ValidationTools()
self.report_tools = ReportTools()
async def initialize(self):
"""Initialize server."""
logger.info("Contract Validator MCP Server initialized")
def setup_tools(self):
"""Register all available tools with the MCP server"""
@self.server.list_tools()
async def list_tools() -> list[Tool]:
"""Return list of available tools"""
tools = [
# Parse tools (to be implemented in #186)
Tool(
name="parse_plugin_interface",
description="Parse plugin README.md to extract interface declarations (inputs, outputs, tools)",
inputSchema={
"type": "object",
"properties": {
"plugin_path": {
"type": "string",
"description": "Path to plugin directory or README.md"
}
},
"required": ["plugin_path"]
}
),
Tool(
name="parse_claude_md_agents",
description="Parse Claude.md to extract agent definitions and their tool sequences",
inputSchema={
"type": "object",
"properties": {
"claude_md_path": {
"type": "string",
"description": "Path to CLAUDE.md file"
}
},
"required": ["claude_md_path"]
}
),
# Validation tools (to be implemented in #187)
Tool(
name="validate_compatibility",
description="Validate compatibility between two plugin interfaces",
inputSchema={
"type": "object",
"properties": {
"plugin_a": {
"type": "string",
"description": "Path to first plugin"
},
"plugin_b": {
"type": "string",
"description": "Path to second plugin"
}
},
"required": ["plugin_a", "plugin_b"]
}
),
Tool(
name="validate_agent_refs",
description="Validate that all tool references in an agent definition exist",
inputSchema={
"type": "object",
"properties": {
"agent_name": {
"type": "string",
"description": "Name of agent to validate"
},
"claude_md_path": {
"type": "string",
"description": "Path to CLAUDE.md containing agent"
},
"plugin_paths": {
"type": "array",
"items": {"type": "string"},
"description": "Paths to available plugins"
}
},
"required": ["agent_name", "claude_md_path"]
}
),
Tool(
name="validate_data_flow",
description="Validate data flow through an agent's tool sequence",
inputSchema={
"type": "object",
"properties": {
"agent_name": {
"type": "string",
"description": "Name of agent to validate"
},
"claude_md_path": {
"type": "string",
"description": "Path to CLAUDE.md containing agent"
}
},
"required": ["agent_name", "claude_md_path"]
}
),
Tool(
name="validate_workflow_integration",
description="Validate that a domain plugin exposes the required advisory interfaces (gate command, review command, advisory agent) expected by projman's domain-consultation skill. Also checks gate contract version compatibility.",
inputSchema={
"type": "object",
"properties": {
"plugin_path": {
"type": "string",
"description": "Path to the domain plugin directory"
},
"domain_label": {
"type": "string",
"description": "The Domain/* label it claims to handle, e.g. Domain/Viz"
},
"expected_contract": {
"type": "string",
"description": "Expected contract version (e.g., 'v1'). If provided, validates the gate command's contract matches."
}
},
"required": ["plugin_path", "domain_label"]
}
),
# Report tools (to be implemented in #188)
Tool(
name="generate_compatibility_report",
description="Generate a comprehensive compatibility report for all plugins",
inputSchema={
"type": "object",
"properties": {
"marketplace_path": {
"type": "string",
"description": "Path to marketplace root directory"
},
"format": {
"type": "string",
"enum": ["markdown", "json"],
"default": "markdown",
"description": "Output format"
}
},
"required": ["marketplace_path"]
}
),
Tool(
name="list_issues",
description="List validation issues with optional filtering",
inputSchema={
"type": "object",
"properties": {
"marketplace_path": {
"type": "string",
"description": "Path to marketplace root directory"
},
"severity": {
"type": "string",
"enum": ["error", "warning", "info", "all"],
"default": "all",
"description": "Filter by severity"
},
"issue_type": {
"type": "string",
"enum": ["missing_tool", "interface_mismatch", "optional_dependency", "undeclared_output", "all"],
"default": "all",
"description": "Filter by issue type"
}
},
"required": ["marketplace_path"]
}
)
]
return tools
@self.server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
"""Handle tool invocation."""
try:
# All tools return placeholder responses for now
# Actual implementation will be added in issues #186, #187, #188
if name == "parse_plugin_interface":
result = await self._parse_plugin_interface(**arguments)
elif name == "parse_claude_md_agents":
result = await self._parse_claude_md_agents(**arguments)
elif name == "validate_compatibility":
result = await self._validate_compatibility(**arguments)
elif name == "validate_agent_refs":
result = await self._validate_agent_refs(**arguments)
elif name == "validate_data_flow":
result = await self._validate_data_flow(**arguments)
elif name == "validate_workflow_integration":
result = await self._validate_workflow_integration(**arguments)
elif name == "generate_compatibility_report":
result = await self._generate_compatibility_report(**arguments)
elif name == "list_issues":
result = await self._list_issues(**arguments)
else:
raise ValueError(f"Unknown tool: {name}")
return [TextContent(
type="text",
text=json.dumps(result, indent=2, default=str)
)]
except Exception as e:
logger.error(f"Tool {name} failed: {e}")
return [TextContent(
type="text",
text=json.dumps({"error": str(e)}, indent=2)
)]
# Parse tool implementations (Issue #186)
async def _parse_plugin_interface(self, plugin_path: str) -> dict:
"""Parse plugin interface from README.md"""
return await self.parse_tools.parse_plugin_interface(plugin_path)
async def _parse_claude_md_agents(self, claude_md_path: str) -> dict:
"""Parse agents from CLAUDE.md"""
return await self.parse_tools.parse_claude_md_agents(claude_md_path)
# Validation tool implementations (Issue #187)
async def _validate_compatibility(self, plugin_a: str, plugin_b: str) -> dict:
"""Validate compatibility between plugins"""
return await self.validation_tools.validate_compatibility(plugin_a, plugin_b)
async def _validate_agent_refs(self, agent_name: str, claude_md_path: str, plugin_paths: list = None) -> dict:
"""Validate agent tool references"""
return await self.validation_tools.validate_agent_refs(agent_name, claude_md_path, plugin_paths)
async def _validate_data_flow(self, agent_name: str, claude_md_path: str) -> dict:
"""Validate agent data flow"""
return await self.validation_tools.validate_data_flow(agent_name, claude_md_path)
async def _validate_workflow_integration(
self,
plugin_path: str,
domain_label: str,
expected_contract: str = None
) -> dict:
"""Validate domain plugin exposes required advisory interfaces"""
return await self.validation_tools.validate_workflow_integration(
plugin_path, domain_label, expected_contract
)
# Report tool implementations (Issue #188)
async def _generate_compatibility_report(self, marketplace_path: str, format: str = "markdown") -> dict:
"""Generate comprehensive compatibility report"""
return await self.report_tools.generate_compatibility_report(marketplace_path, format)
async def _list_issues(self, marketplace_path: str, severity: str = "all", issue_type: str = "all") -> dict:
"""List validation issues with filtering"""
return await self.report_tools.list_issues(marketplace_path, severity, issue_type)
async def run(self):
"""Run the MCP server"""
await self.initialize()
self.setup_tools()
async with stdio_server() as (read_stream, write_stream):
await self.server.run(
read_stream,
write_stream,
self.server.create_initialization_options()
)
async def main():
"""Main entry point"""
server = ContractValidatorMCPServer()
await server.run()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -1,493 +0,0 @@
"""
Validation tools for checking cross-plugin compatibility and agent references.
Provides:
- validate_compatibility: Compare two plugin interfaces
- validate_agent_refs: Check agent tool references exist
- validate_data_flow: Verify data flow through agent sequences
"""
from pathlib import Path
from typing import Optional
from pydantic import BaseModel
from enum import Enum
from .parse_tools import ParseTools, PluginInterface, ClaudeMdAgent
class IssueSeverity(str, Enum):
ERROR = "error"
WARNING = "warning"
INFO = "info"
class IssueType(str, Enum):
MISSING_TOOL = "missing_tool"
INTERFACE_MISMATCH = "interface_mismatch"
OPTIONAL_DEPENDENCY = "optional_dependency"
UNDECLARED_OUTPUT = "undeclared_output"
INVALID_SEQUENCE = "invalid_sequence"
MISSING_INTEGRATION = "missing_integration"
class ValidationIssue(BaseModel):
"""A single validation issue"""
severity: IssueSeverity
issue_type: IssueType
message: str
location: Optional[str] = None
suggestion: Optional[str] = None
class CompatibilityResult(BaseModel):
"""Result of compatibility check between two plugins"""
plugin_a: str
plugin_b: str
compatible: bool
shared_tools: list[str] = []
a_only_tools: list[str] = []
b_only_tools: list[str] = []
issues: list[ValidationIssue] = []
class AgentValidationResult(BaseModel):
"""Result of agent reference validation"""
agent_name: str
valid: bool
tool_refs_found: list[str] = []
tool_refs_missing: list[str] = []
issues: list[ValidationIssue] = []
class DataFlowResult(BaseModel):
"""Result of data flow validation"""
agent_name: str
valid: bool
flow_steps: list[str] = []
issues: list[ValidationIssue] = []
class WorkflowIntegrationResult(BaseModel):
"""Result of workflow integration validation for domain plugins"""
plugin_name: str
domain_label: str
valid: bool
gate_command_found: bool
gate_contract: Optional[str] = None # Contract version declared by gate command
review_command_found: bool
advisory_agent_found: bool
issues: list[ValidationIssue] = []
class ValidationTools:
"""Tools for validating plugin compatibility and agent references"""
def __init__(self):
self.parse_tools = ParseTools()
async def validate_compatibility(self, plugin_a: str, plugin_b: str) -> dict:
"""
Validate compatibility between two plugin interfaces.
Compares tools, commands, and agents to identify overlaps and gaps.
Args:
plugin_a: Path to first plugin directory
plugin_b: Path to second plugin directory
Returns:
Compatibility report with shared tools, unique tools, and issues
"""
# Parse both plugins
interface_a = await self.parse_tools.parse_plugin_interface(plugin_a)
interface_b = await self.parse_tools.parse_plugin_interface(plugin_b)
# Check for parse errors
if "error" in interface_a:
return {
"error": f"Failed to parse plugin A: {interface_a['error']}",
"plugin_a": plugin_a,
"plugin_b": plugin_b
}
if "error" in interface_b:
return {
"error": f"Failed to parse plugin B: {interface_b['error']}",
"plugin_a": plugin_a,
"plugin_b": plugin_b
}
# Extract tool names
tools_a = set(t["name"] for t in interface_a.get("tools", []))
tools_b = set(t["name"] for t in interface_b.get("tools", []))
# Find overlaps and differences
shared = tools_a & tools_b
a_only = tools_a - tools_b
b_only = tools_b - tools_a
issues = []
# Check for potential naming conflicts
if shared:
issues.append(ValidationIssue(
severity=IssueSeverity.WARNING,
issue_type=IssueType.INTERFACE_MISMATCH,
message=f"Both plugins define tools with same names: {list(shared)}",
location=f"{interface_a['plugin_name']} and {interface_b['plugin_name']}",
suggestion="Ensure tools with same names have compatible interfaces"
))
# Check command overlaps
cmds_a = set(c["name"] for c in interface_a.get("commands", []))
cmds_b = set(c["name"] for c in interface_b.get("commands", []))
shared_cmds = cmds_a & cmds_b
if shared_cmds:
issues.append(ValidationIssue(
severity=IssueSeverity.ERROR,
issue_type=IssueType.INTERFACE_MISMATCH,
message=f"Command name conflict: {list(shared_cmds)}",
location=f"{interface_a['plugin_name']} and {interface_b['plugin_name']}",
suggestion="Rename conflicting commands to avoid ambiguity"
))
result = CompatibilityResult(
plugin_a=interface_a["plugin_name"],
plugin_b=interface_b["plugin_name"],
compatible=len([i for i in issues if i.severity == IssueSeverity.ERROR]) == 0,
shared_tools=list(shared),
a_only_tools=list(a_only),
b_only_tools=list(b_only),
issues=issues
)
return result.model_dump()
async def validate_agent_refs(
self,
agent_name: str,
claude_md_path: str,
plugin_paths: list[str] = None
) -> dict:
"""
Validate that all tool references in an agent definition exist.
Args:
agent_name: Name of the agent to validate
claude_md_path: Path to CLAUDE.md containing the agent
plugin_paths: Optional list of plugin paths to check for tools
Returns:
Validation result with found/missing tools and issues
"""
# Parse CLAUDE.md for agents
agents_result = await self.parse_tools.parse_claude_md_agents(claude_md_path)
if "error" in agents_result:
return {
"error": agents_result["error"],
"agent_name": agent_name
}
# Find the specific agent
agent = None
for a in agents_result.get("agents", []):
if a["name"].lower() == agent_name.lower():
agent = a
break
if not agent:
return {
"error": f"Agent '{agent_name}' not found in {claude_md_path}",
"agent_name": agent_name,
"available_agents": [a["name"] for a in agents_result.get("agents", [])]
}
# Collect all available tools from plugins
available_tools = set()
if plugin_paths:
for plugin_path in plugin_paths:
interface = await self.parse_tools.parse_plugin_interface(plugin_path)
if "error" not in interface:
for tool in interface.get("tools", []):
available_tools.add(tool["name"])
# Check agent tool references
tool_refs = set(agent.get("tool_refs", []))
found = tool_refs & available_tools if available_tools else tool_refs
missing = tool_refs - available_tools if available_tools else set()
issues = []
# Report missing tools
for tool in missing:
issues.append(ValidationIssue(
severity=IssueSeverity.ERROR,
issue_type=IssueType.MISSING_TOOL,
message=f"Agent '{agent_name}' references tool '{tool}' which is not found",
location=claude_md_path,
suggestion=f"Check if tool '{tool}' exists or fix the reference"
))
# Check if agent has no tool refs (might be incomplete)
if not tool_refs:
issues.append(ValidationIssue(
severity=IssueSeverity.INFO,
issue_type=IssueType.UNDECLARED_OUTPUT,
message=f"Agent '{agent_name}' has no documented tool references",
location=claude_md_path,
suggestion="Consider documenting which tools this agent uses"
))
result = AgentValidationResult(
agent_name=agent_name,
valid=len([i for i in issues if i.severity == IssueSeverity.ERROR]) == 0,
tool_refs_found=list(found),
tool_refs_missing=list(missing),
issues=issues
)
return result.model_dump()
async def validate_data_flow(self, agent_name: str, claude_md_path: str) -> dict:
"""
Validate data flow through an agent's tool sequence.
Checks that each step's expected output can be used by the next step.
Args:
agent_name: Name of the agent to validate
claude_md_path: Path to CLAUDE.md containing the agent
Returns:
Data flow validation result with steps and issues
"""
# Parse CLAUDE.md for agents
agents_result = await self.parse_tools.parse_claude_md_agents(claude_md_path)
if "error" in agents_result:
return {
"error": agents_result["error"],
"agent_name": agent_name
}
# Find the specific agent
agent = None
for a in agents_result.get("agents", []):
if a["name"].lower() == agent_name.lower():
agent = a
break
if not agent:
return {
"error": f"Agent '{agent_name}' not found in {claude_md_path}",
"agent_name": agent_name,
"available_agents": [a["name"] for a in agents_result.get("agents", [])]
}
issues = []
flow_steps = []
# Extract workflow steps
workflow_steps = agent.get("workflow_steps", [])
responsibilities = agent.get("responsibilities", [])
# Build flow from workflow steps or responsibilities
steps = workflow_steps if workflow_steps else responsibilities
for i, step in enumerate(steps):
flow_steps.append(f"Step {i+1}: {step}")
# Check for data flow patterns
tool_refs = agent.get("tool_refs", [])
# Known data flow patterns
# e.g., data-platform produces data_ref, viz-platform consumes it
known_producers = {
"read_csv": "data_ref",
"read_parquet": "data_ref",
"pg_query": "data_ref",
"filter": "data_ref",
"groupby": "data_ref",
}
known_consumers = {
"describe": "data_ref",
"head": "data_ref",
"tail": "data_ref",
"to_csv": "data_ref",
"to_parquet": "data_ref",
}
# Check if agent uses tools that require data_ref
has_producer = any(t in known_producers for t in tool_refs)
has_consumer = any(t in known_consumers for t in tool_refs)
if has_consumer and not has_producer:
issues.append(ValidationIssue(
severity=IssueSeverity.WARNING,
issue_type=IssueType.INTERFACE_MISMATCH,
message=f"Agent '{agent_name}' uses tools that consume data_ref but no producer found",
location=claude_md_path,
suggestion="Ensure a data loading tool (read_csv, pg_query, etc.) is used before data consumers"
))
# Check for empty workflow
if not steps and not tool_refs:
issues.append(ValidationIssue(
severity=IssueSeverity.INFO,
issue_type=IssueType.UNDECLARED_OUTPUT,
message=f"Agent '{agent_name}' has no documented workflow or tool sequence",
location=claude_md_path,
suggestion="Consider documenting the agent's workflow steps"
))
result = DataFlowResult(
agent_name=agent_name,
valid=len([i for i in issues if i.severity == IssueSeverity.ERROR]) == 0,
flow_steps=flow_steps,
issues=issues
)
return result.model_dump()
async def validate_workflow_integration(
self,
plugin_path: str,
domain_label: str,
expected_contract: Optional[str] = None
) -> dict:
"""
Validate that a domain plugin exposes required advisory interfaces.
Checks for:
- Gate command (e.g., /design-gate, /data-gate) - REQUIRED
- Gate contract version (gate_contract in frontmatter) - INFO if missing
- Review command (e.g., /design-review, /data-review) - recommended
- Advisory agent referencing the domain label - recommended
Args:
plugin_path: Path to the domain plugin directory
domain_label: The Domain/* label it claims to handle (e.g., Domain/Viz)
expected_contract: Expected contract version (e.g., 'v1'). If provided,
validates the gate command's contract matches.
Returns:
Validation result with found interfaces and issues
"""
import re
plugin_path_obj = Path(plugin_path)
issues = []
# Extract plugin name from path
plugin_name = plugin_path_obj.name
if not plugin_path_obj.exists():
return {
"error": f"Plugin directory not found: {plugin_path}",
"plugin_path": plugin_path,
"domain_label": domain_label
}
# Extract domain short name from label (e.g., "Domain/Viz" -> "viz", "Domain/Data" -> "data")
domain_short = domain_label.split("/")[-1].lower() if "/" in domain_label else domain_label.lower()
# Check for gate command
commands_dir = plugin_path_obj / "commands"
gate_command_found = False
gate_contract = None
gate_patterns = ["pass", "fail", "PASS", "FAIL", "Binary pass/fail", "gate"]
if commands_dir.exists():
for cmd_file in commands_dir.glob("*.md"):
if "gate" in cmd_file.name.lower():
# Verify it's actually a gate command by checking content
content = cmd_file.read_text()
if any(pattern in content for pattern in gate_patterns):
gate_command_found = True
# Parse frontmatter for gate_contract
frontmatter_match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
if frontmatter_match:
frontmatter = frontmatter_match.group(1)
contract_match = re.search(r'gate_contract:\s*(\S+)', frontmatter)
if contract_match:
gate_contract = contract_match.group(1)
break
if not gate_command_found:
issues.append(ValidationIssue(
severity=IssueSeverity.ERROR,
issue_type=IssueType.MISSING_INTEGRATION,
message=f"Plugin '{plugin_name}' lacks a gate command for domain '{domain_label}'",
location=str(commands_dir),
suggestion=f"Create commands/{domain_short}-gate.md with binary PASS/FAIL output"
))
# Check for review command
review_command_found = False
if commands_dir.exists():
for cmd_file in commands_dir.glob("*.md"):
if "review" in cmd_file.name.lower() and "gate" not in cmd_file.name.lower():
review_command_found = True
break
if not review_command_found:
issues.append(ValidationIssue(
severity=IssueSeverity.WARNING,
issue_type=IssueType.MISSING_INTEGRATION,
message=f"Plugin '{plugin_name}' lacks a review command for domain '{domain_label}'",
location=str(commands_dir),
suggestion=f"Create commands/{domain_short}-review.md for detailed audits"
))
# Check for advisory agent
agents_dir = plugin_path_obj / "agents"
advisory_agent_found = False
if agents_dir.exists():
for agent_file in agents_dir.glob("*.md"):
content = agent_file.read_text()
# Check if agent references the domain label or gate command
if domain_label in content or f"{domain_short}-gate" in content.lower() or "advisor" in agent_file.name.lower() or "reviewer" in agent_file.name.lower():
advisory_agent_found = True
break
if not advisory_agent_found:
issues.append(ValidationIssue(
severity=IssueSeverity.WARNING,
issue_type=IssueType.MISSING_INTEGRATION,
message=f"Plugin '{plugin_name}' lacks an advisory agent for domain '{domain_label}'",
location=str(agents_dir) if agents_dir.exists() else str(plugin_path_obj),
suggestion=f"Create agents/{domain_short}-advisor.md referencing '{domain_label}'"
))
# Check gate contract version
if gate_command_found:
if not gate_contract:
issues.append(ValidationIssue(
severity=IssueSeverity.INFO,
issue_type=IssueType.MISSING_INTEGRATION,
message=f"Gate command does not declare a contract version",
location=str(commands_dir),
suggestion="Consider adding `gate_contract: v1` to frontmatter for version tracking"
))
elif expected_contract and gate_contract != expected_contract:
issues.append(ValidationIssue(
severity=IssueSeverity.WARNING,
issue_type=IssueType.INTERFACE_MISMATCH,
message=f"Contract version mismatch: gate declares {gate_contract}, projman expects {expected_contract}",
location=str(commands_dir),
suggestion=f"Update domain-consultation.md Gate Command Reference table to {gate_contract}, or update gate command to {expected_contract}"
))
result = WorkflowIntegrationResult(
plugin_name=plugin_name,
domain_label=domain_label,
valid=gate_command_found, # Only gate is required for validity
gate_command_found=gate_command_found,
gate_contract=gate_contract,
review_command_found=review_command_found,
advisory_agent_found=advisory_agent_found,
issues=issues
)
return result.model_dump()

View File

@@ -1,41 +0,0 @@
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "contract-validator-mcp"
version = "1.0.0"
description = "MCP Server for cross-plugin compatibility validation and agent verification"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.10"
authors = [
{name = "Leo Miranda"}
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"mcp>=0.9.0",
"pydantic>=2.5.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.4.3",
"pytest-asyncio>=0.23.0",
]
[tool.setuptools.packages.find]
where = ["."]
include = ["mcp_server*"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

View File

@@ -1,9 +0,0 @@
# MCP SDK
mcp>=0.9.0
# Utilities
pydantic>=2.5.0
# Testing
pytest>=7.4.3
pytest-asyncio>=0.23.0

View File

@@ -1,21 +0,0 @@
#!/bin/bash
# Capture original working directory before any cd operations
# This should be the user's project directory when launched by Claude Code
export CLAUDE_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CACHE_VENV="$HOME/.cache/claude-mcp-venvs/leo-claude-mktplace/contract-validator/.venv"
LOCAL_VENV="$SCRIPT_DIR/.venv"
if [[ -f "$CACHE_VENV/bin/python" ]]; then
PYTHON="$CACHE_VENV/bin/python"
elif [[ -f "$LOCAL_VENV/bin/python" ]]; then
PYTHON="$LOCAL_VENV/bin/python"
else
echo "ERROR: No venv found. Run: ./scripts/setup-venvs.sh" >&2
exit 1
fi
cd "$SCRIPT_DIR"
export PYTHONPATH="$SCRIPT_DIR"
exec "$PYTHON" -m mcp_server.server "$@"

View File

@@ -1 +0,0 @@
# Tests for contract-validator MCP server

View File

@@ -1,193 +0,0 @@
"""
Unit tests for parse tools.
"""
import pytest
from pathlib import Path
@pytest.fixture
def parse_tools():
"""Create ParseTools instance"""
from mcp_server.parse_tools import ParseTools
return ParseTools()
@pytest.fixture
def sample_readme(tmp_path):
"""Create a sample README.md for testing"""
readme = tmp_path / "README.md"
readme.write_text("""# Test Plugin
A test plugin for validation.
## Features
- **Feature One**: Does something
- **Feature Two**: Does something else
## Commands
| Command | Description |
|---------|-------------|
| `/test-cmd` | Test command |
| `/another-cmd` | Another test command |
## Agents
| Agent | Description |
|-------|-------------|
| `test-agent` | A test agent |
## Tools Summary
### Category A (3 tools)
`tool_a`, `tool_b`, `tool_c`
### Category B (2 tools)
`tool_d`, `tool_e`
""")
return str(tmp_path)
@pytest.fixture
def sample_claude_md(tmp_path):
"""Create a sample CLAUDE.md for testing"""
claude_md = tmp_path / "CLAUDE.md"
claude_md.write_text("""# CLAUDE.md
## Project Overview
### Four-Agent Model (test)
| Agent | Personality | Responsibilities |
|-------|-------------|------------------|
| **Planner** | Thoughtful | Planning via `create_issue`, `search_lessons` |
| **Executor** | Focused | Implementation via `write`, `edit` |
## Workflow
1. Planner creates issues
2. Executor implements code
""")
return str(claude_md)
@pytest.mark.asyncio
async def test_parse_plugin_interface_basic(parse_tools, sample_readme):
"""Test basic plugin interface parsing"""
result = await parse_tools.parse_plugin_interface(sample_readme)
assert "error" not in result
# Plugin name extraction strips "Plugin" suffix
assert result["plugin_name"] == "Test"
assert "A test plugin" in result["description"]
@pytest.mark.asyncio
async def test_parse_plugin_interface_commands(parse_tools, sample_readme):
"""Test command extraction from README"""
result = await parse_tools.parse_plugin_interface(sample_readme)
commands = result["commands"]
assert len(commands) == 2
assert commands[0]["name"] == "/test-cmd"
assert commands[1]["name"] == "/another-cmd"
@pytest.mark.asyncio
async def test_parse_plugin_interface_agents(parse_tools, sample_readme):
"""Test agent extraction from README"""
result = await parse_tools.parse_plugin_interface(sample_readme)
agents = result["agents"]
assert len(agents) == 1
assert agents[0]["name"] == "test-agent"
@pytest.mark.asyncio
async def test_parse_plugin_interface_tools(parse_tools, sample_readme):
"""Test tool extraction from README"""
result = await parse_tools.parse_plugin_interface(sample_readme)
tools = result["tools"]
tool_names = [t["name"] for t in tools]
assert "tool_a" in tool_names
assert "tool_b" in tool_names
assert "tool_e" in tool_names
assert len(tools) >= 5
@pytest.mark.asyncio
async def test_parse_plugin_interface_categories(parse_tools, sample_readme):
"""Test tool category extraction"""
result = await parse_tools.parse_plugin_interface(sample_readme)
categories = result["tool_categories"]
assert "Category A" in categories
assert "Category B" in categories
assert "tool_a" in categories["Category A"]
@pytest.mark.asyncio
async def test_parse_plugin_interface_features(parse_tools, sample_readme):
"""Test feature extraction"""
result = await parse_tools.parse_plugin_interface(sample_readme)
features = result["features"]
assert "Feature One" in features
assert "Feature Two" in features
@pytest.mark.asyncio
async def test_parse_plugin_interface_not_found(parse_tools, tmp_path):
"""Test error when README not found"""
result = await parse_tools.parse_plugin_interface(str(tmp_path / "nonexistent"))
assert "error" in result
assert "not found" in result["error"].lower()
@pytest.mark.asyncio
async def test_parse_claude_md_agents(parse_tools, sample_claude_md):
"""Test agent extraction from CLAUDE.md"""
result = await parse_tools.parse_claude_md_agents(sample_claude_md)
assert "error" not in result
assert result["agent_count"] == 2
agents = result["agents"]
agent_names = [a["name"] for a in agents]
assert "Planner" in agent_names
assert "Executor" in agent_names
@pytest.mark.asyncio
async def test_parse_claude_md_tool_refs(parse_tools, sample_claude_md):
"""Test tool reference extraction from agents"""
result = await parse_tools.parse_claude_md_agents(sample_claude_md)
agents = {a["name"]: a for a in result["agents"]}
planner = agents["Planner"]
assert "create_issue" in planner["tool_refs"]
assert "search_lessons" in planner["tool_refs"]
@pytest.mark.asyncio
async def test_parse_claude_md_not_found(parse_tools, tmp_path):
"""Test error when CLAUDE.md not found"""
result = await parse_tools.parse_claude_md_agents(str(tmp_path / "CLAUDE.md"))
assert "error" in result
assert "not found" in result["error"].lower()
@pytest.mark.asyncio
async def test_parse_plugin_with_direct_file(parse_tools, sample_readme):
"""Test parsing with direct file path instead of directory"""
readme_path = Path(sample_readme) / "README.md"
result = await parse_tools.parse_plugin_interface(str(readme_path))
assert "error" not in result
# Plugin name extraction strips "Plugin" suffix
assert result["plugin_name"] == "Test"

View File

@@ -1,261 +0,0 @@
"""
Unit tests for report tools.
"""
import pytest
from pathlib import Path
@pytest.fixture
def report_tools():
"""Create ReportTools instance"""
from mcp_server.report_tools import ReportTools
return ReportTools()
@pytest.fixture
def sample_marketplace(tmp_path):
"""Create a sample marketplace structure"""
import json
plugins_dir = tmp_path / "plugins"
plugins_dir.mkdir()
# Plugin 1
plugin1 = plugins_dir / "plugin-one"
plugin1.mkdir()
plugin1_meta = plugin1 / ".claude-plugin"
plugin1_meta.mkdir()
(plugin1_meta / "plugin.json").write_text(json.dumps({"name": "plugin-one"}))
(plugin1 / "README.md").write_text("""# plugin-one
First test plugin.
## Commands
| Command | Description |
|---------|-------------|
| `/cmd-one` | Command one |
## Tools Summary
### Tools (2 tools)
`tool_a`, `tool_b`
""")
# Plugin 2
plugin2 = plugins_dir / "plugin-two"
plugin2.mkdir()
plugin2_meta = plugin2 / ".claude-plugin"
plugin2_meta.mkdir()
(plugin2_meta / "plugin.json").write_text(json.dumps({"name": "plugin-two"}))
(plugin2 / "README.md").write_text("""# plugin-two
Second test plugin.
## Commands
| Command | Description |
|---------|-------------|
| `/cmd-two` | Command two |
## Tools Summary
### Tools (2 tools)
`tool_c`, `tool_d`
""")
# Plugin 3 (with conflict)
plugin3 = plugins_dir / "plugin-three"
plugin3.mkdir()
plugin3_meta = plugin3 / ".claude-plugin"
plugin3_meta.mkdir()
(plugin3_meta / "plugin.json").write_text(json.dumps({"name": "plugin-three"}))
(plugin3 / "README.md").write_text("""# plugin-three
Third test plugin with conflict.
## Commands
| Command | Description |
|---------|-------------|
| `/cmd-one` | Conflicting command |
## Tools Summary
### Tools (1 tool)
`tool_e`
""")
return str(tmp_path)
@pytest.fixture
def marketplace_no_plugins(tmp_path):
"""Create marketplace with no plugins"""
plugins_dir = tmp_path / "plugins"
plugins_dir.mkdir()
return str(tmp_path)
@pytest.fixture
def marketplace_no_dir(tmp_path):
"""Create path without plugins directory"""
return str(tmp_path)
@pytest.mark.asyncio
async def test_generate_report_json_format(report_tools, sample_marketplace):
"""Test JSON format report generation"""
result = await report_tools.generate_compatibility_report(
sample_marketplace, "json"
)
assert "error" not in result
assert "generated_at" in result
assert "summary" in result
assert "plugins" in result
assert result["summary"]["total_plugins"] == 3
@pytest.mark.asyncio
async def test_generate_report_markdown_format(report_tools, sample_marketplace):
"""Test markdown format report generation"""
result = await report_tools.generate_compatibility_report(
sample_marketplace, "markdown"
)
assert "error" not in result
assert "report" in result
assert "# Contract Validation Report" in result["report"]
assert "## Summary" in result["report"]
@pytest.mark.asyncio
async def test_generate_report_finds_conflicts(report_tools, sample_marketplace):
"""Test that report finds command conflicts"""
result = await report_tools.generate_compatibility_report(
sample_marketplace, "json"
)
assert "error" not in result
assert result["summary"]["errors"] > 0
assert result["summary"]["total_issues"] > 0
@pytest.mark.asyncio
async def test_generate_report_counts_correctly(report_tools, sample_marketplace):
"""Test summary counts are correct"""
result = await report_tools.generate_compatibility_report(
sample_marketplace, "json"
)
summary = result["summary"]
assert summary["total_plugins"] == 3
assert summary["total_commands"] == 3 # 3 commands total
assert summary["total_tools"] == 5 # a, b, c, d, e
@pytest.mark.asyncio
async def test_generate_report_no_plugins(report_tools, marketplace_no_plugins):
"""Test error when no plugins found"""
result = await report_tools.generate_compatibility_report(
marketplace_no_plugins, "json"
)
assert "error" in result
assert "no plugins" in result["error"].lower()
@pytest.mark.asyncio
async def test_generate_report_no_plugins_dir(report_tools, marketplace_no_dir):
"""Test error when plugins directory doesn't exist"""
result = await report_tools.generate_compatibility_report(
marketplace_no_dir, "json"
)
assert "error" in result
assert "not found" in result["error"].lower()
@pytest.mark.asyncio
async def test_list_issues_all(report_tools, sample_marketplace):
"""Test listing all issues"""
result = await report_tools.list_issues(sample_marketplace, "all", "all")
assert "error" not in result
assert "issues" in result
assert result["total_issues"] > 0
@pytest.mark.asyncio
async def test_list_issues_filter_by_severity(report_tools, sample_marketplace):
"""Test filtering issues by severity"""
all_result = await report_tools.list_issues(sample_marketplace, "all", "all")
error_result = await report_tools.list_issues(sample_marketplace, "error", "all")
# Error count should be less than or equal to all
assert error_result["total_issues"] <= all_result["total_issues"]
# All issues should have error severity
for issue in error_result["issues"]:
sev = issue.get("severity", "")
if hasattr(sev, 'value'):
sev = sev.value
assert "error" in str(sev).lower()
@pytest.mark.asyncio
async def test_list_issues_filter_by_type(report_tools, sample_marketplace):
"""Test filtering issues by type"""
result = await report_tools.list_issues(
sample_marketplace, "all", "interface_mismatch"
)
# All issues should have matching type
for issue in result["issues"]:
itype = issue.get("issue_type", "")
if hasattr(itype, 'value'):
itype = itype.value
assert "interface_mismatch" in str(itype).lower()
@pytest.mark.asyncio
async def test_list_issues_combined_filters(report_tools, sample_marketplace):
"""Test combined severity and type filters"""
result = await report_tools.list_issues(
sample_marketplace, "error", "interface_mismatch"
)
assert "error" not in result
# Should have command conflict errors
assert result["total_issues"] > 0
@pytest.mark.asyncio
async def test_report_markdown_has_all_sections(report_tools, sample_marketplace):
"""Test markdown report contains all expected sections"""
result = await report_tools.generate_compatibility_report(
sample_marketplace, "markdown"
)
report = result["report"]
assert "## Summary" in report
assert "## Plugins" in report
# Compatibility section only if there are checks
assert "Plugin One" in report or "plugin-one" in report.lower()
@pytest.mark.asyncio
async def test_report_includes_suggestions(report_tools, sample_marketplace):
"""Test that issues include suggestions"""
result = await report_tools.generate_compatibility_report(
sample_marketplace, "json"
)
issues = result.get("all_issues", [])
# Find an issue with a suggestion
issues_with_suggestions = [
i for i in issues
if i.get("suggestion")
]
assert len(issues_with_suggestions) > 0

View File

@@ -1,514 +0,0 @@
"""
Unit tests for validation tools.
"""
import pytest
from pathlib import Path
@pytest.fixture
def validation_tools():
"""Create ValidationTools instance"""
from mcp_server.validation_tools import ValidationTools
return ValidationTools()
@pytest.fixture
def plugin_a(tmp_path):
"""Create first test plugin"""
plugin_dir = tmp_path / "plugin-a"
plugin_dir.mkdir()
(plugin_dir / ".claude-plugin").mkdir()
readme = plugin_dir / "README.md"
readme.write_text("""# Plugin A
Test plugin A.
## Commands
| Command | Description |
|---------|-------------|
| `/setup-a` | Setup A |
| `/shared-cmd` | Shared command |
## Tools Summary
### Core (2 tools)
`tool_one`, `tool_two`
""")
return str(plugin_dir)
@pytest.fixture
def plugin_b(tmp_path):
"""Create second test plugin"""
plugin_dir = tmp_path / "plugin-b"
plugin_dir.mkdir()
(plugin_dir / ".claude-plugin").mkdir()
readme = plugin_dir / "README.md"
readme.write_text("""# Plugin B
Test plugin B.
## Commands
| Command | Description |
|---------|-------------|
| `/setup-b` | Setup B |
| `/shared-cmd` | Shared command (conflict!) |
## Tools Summary
### Core (2 tools)
`tool_two`, `tool_three`
""")
return str(plugin_dir)
@pytest.fixture
def plugin_no_conflict(tmp_path):
"""Create plugin with no conflicts"""
plugin_dir = tmp_path / "plugin-c"
plugin_dir.mkdir()
(plugin_dir / ".claude-plugin").mkdir()
readme = plugin_dir / "README.md"
readme.write_text("""# Plugin C
Test plugin C.
## Commands
| Command | Description |
|---------|-------------|
| `/unique-cmd` | Unique command |
## Tools Summary
### Core (1 tool)
`unique_tool`
""")
return str(plugin_dir)
@pytest.fixture
def claude_md_with_agents(tmp_path):
"""Create CLAUDE.md with agent definitions"""
claude_md = tmp_path / "CLAUDE.md"
claude_md.write_text("""# CLAUDE.md
### Four-Agent Model
| Agent | Personality | Responsibilities |
|-------|-------------|------------------|
| **TestAgent** | Careful | Uses `tool_one`, `tool_two`, `missing_tool` |
| **ValidAgent** | Thorough | Uses `tool_one` only |
| **EmptyAgent** | Unknown | General tasks |
""")
return str(claude_md)
@pytest.mark.asyncio
async def test_validate_compatibility_command_conflict(validation_tools, plugin_a, plugin_b):
"""Test detection of command name conflicts"""
result = await validation_tools.validate_compatibility(plugin_a, plugin_b)
assert "error" not in result
assert result["compatible"] is False
# Find the command conflict issue
error_issues = [i for i in result["issues"] if i["severity"].value == "error"]
assert len(error_issues) > 0
assert any("/shared-cmd" in str(i["message"]) for i in error_issues)
@pytest.mark.asyncio
async def test_validate_compatibility_tool_overlap(validation_tools, plugin_a, plugin_b):
"""Test detection of tool name overlaps"""
result = await validation_tools.validate_compatibility(plugin_a, plugin_b)
assert "tool_two" in result["shared_tools"]
@pytest.mark.asyncio
async def test_validate_compatibility_unique_tools(validation_tools, plugin_a, plugin_b):
"""Test identification of unique tools per plugin"""
result = await validation_tools.validate_compatibility(plugin_a, plugin_b)
assert "tool_one" in result["a_only_tools"]
assert "tool_three" in result["b_only_tools"]
@pytest.mark.asyncio
async def test_validate_compatibility_no_conflict(validation_tools, plugin_a, plugin_no_conflict):
"""Test compatible plugins"""
result = await validation_tools.validate_compatibility(plugin_a, plugin_no_conflict)
assert "error" not in result
assert result["compatible"] is True
@pytest.mark.asyncio
async def test_validate_compatibility_missing_plugin(validation_tools, plugin_a, tmp_path):
"""Test error when plugin not found"""
result = await validation_tools.validate_compatibility(
plugin_a,
str(tmp_path / "nonexistent")
)
assert "error" in result
@pytest.mark.asyncio
async def test_validate_agent_refs_with_missing_tools(validation_tools, claude_md_with_agents, plugin_a):
"""Test detection of missing tool references"""
result = await validation_tools.validate_agent_refs(
"TestAgent",
claude_md_with_agents,
[plugin_a]
)
assert "error" not in result
assert result["valid"] is False
assert "missing_tool" in result["tool_refs_missing"]
@pytest.mark.asyncio
async def test_validate_agent_refs_valid_agent(validation_tools, claude_md_with_agents, plugin_a):
"""Test valid agent with all tools found"""
result = await validation_tools.validate_agent_refs(
"ValidAgent",
claude_md_with_agents,
[plugin_a]
)
assert "error" not in result
assert result["valid"] is True
assert "tool_one" in result["tool_refs_found"]
@pytest.mark.asyncio
async def test_validate_agent_refs_empty_agent(validation_tools, claude_md_with_agents, plugin_a):
"""Test agent with no tool references"""
result = await validation_tools.validate_agent_refs(
"EmptyAgent",
claude_md_with_agents,
[plugin_a]
)
assert "error" not in result
# Should have info issue about undocumented references
info_issues = [i for i in result["issues"] if i["severity"].value == "info"]
assert len(info_issues) > 0
@pytest.mark.asyncio
async def test_validate_agent_refs_agent_not_found(validation_tools, claude_md_with_agents, plugin_a):
"""Test error when agent not found"""
result = await validation_tools.validate_agent_refs(
"NonexistentAgent",
claude_md_with_agents,
[plugin_a]
)
assert "error" in result
assert "not found" in result["error"].lower()
@pytest.mark.asyncio
async def test_validate_data_flow_valid(validation_tools, tmp_path):
"""Test data flow validation with valid flow"""
claude_md = tmp_path / "CLAUDE.md"
claude_md.write_text("""# CLAUDE.md
### Four-Agent Model
| Agent | Personality | Responsibilities |
|-------|-------------|------------------|
| **DataAgent** | Analytical | Load with `read_csv`, analyze with `describe`, export with `to_csv` |
""")
result = await validation_tools.validate_data_flow("DataAgent", str(claude_md))
assert "error" not in result
assert result["valid"] is True
@pytest.mark.asyncio
async def test_validate_data_flow_missing_producer(validation_tools, tmp_path):
"""Test data flow with consumer but no producer"""
claude_md = tmp_path / "CLAUDE.md"
claude_md.write_text("""# CLAUDE.md
### Four-Agent Model
| Agent | Personality | Responsibilities |
|-------|-------------|------------------|
| **BadAgent** | Careless | Just runs `describe`, `head`, `tail` without loading |
""")
result = await validation_tools.validate_data_flow("BadAgent", str(claude_md))
assert "error" not in result
# Should have warning about missing producer
warning_issues = [i for i in result["issues"] if i["severity"].value == "warning"]
assert len(warning_issues) > 0
# --- Workflow Integration Tests ---
@pytest.fixture
def domain_plugin_complete(tmp_path):
"""Create a complete domain plugin with gate, review, and advisory agent"""
plugin_dir = tmp_path / "viz-platform"
plugin_dir.mkdir()
(plugin_dir / ".claude-plugin").mkdir()
(plugin_dir / "commands").mkdir()
(plugin_dir / "agents").mkdir()
# Gate command with PASS/FAIL pattern
gate_cmd = plugin_dir / "commands" / "design-gate.md"
gate_cmd.write_text("""# /design-gate
Binary pass/fail validation gate for design system compliance.
## Output
- **PASS**: All design system checks passed
- **FAIL**: Design system violations detected
""")
# Review command
review_cmd = plugin_dir / "commands" / "design-review.md"
review_cmd.write_text("""# /design-review
Comprehensive design system audit.
""")
# Advisory agent
agent = plugin_dir / "agents" / "design-reviewer.md"
agent.write_text("""# design-reviewer
Design system compliance auditor.
Handles issues with `Domain/Viz` label.
""")
return str(plugin_dir)
@pytest.fixture
def domain_plugin_missing_gate(tmp_path):
"""Create domain plugin with review and agent but no gate command"""
plugin_dir = tmp_path / "data-platform"
plugin_dir.mkdir()
(plugin_dir / ".claude-plugin").mkdir()
(plugin_dir / "commands").mkdir()
(plugin_dir / "agents").mkdir()
# Review command (but no gate)
review_cmd = plugin_dir / "commands" / "data-review.md"
review_cmd.write_text("""# /data-review
Data integrity audit.
""")
# Advisory agent
agent = plugin_dir / "agents" / "data-advisor.md"
agent.write_text("""# data-advisor
Data integrity advisor for Domain/Data issues.
""")
return str(plugin_dir)
@pytest.fixture
def domain_plugin_minimal(tmp_path):
"""Create minimal plugin with no commands or agents"""
plugin_dir = tmp_path / "minimal-plugin"
plugin_dir.mkdir()
(plugin_dir / ".claude-plugin").mkdir()
readme = plugin_dir / "README.md"
readme.write_text("# Minimal Plugin\n\nNo commands or agents.")
return str(plugin_dir)
@pytest.mark.asyncio
async def test_validate_workflow_integration_complete(validation_tools, domain_plugin_complete):
"""Test complete domain plugin returns valid with all interfaces found"""
result = await validation_tools.validate_workflow_integration(
domain_plugin_complete,
"Domain/Viz"
)
assert "error" not in result
assert result["valid"] is True
assert result["gate_command_found"] is True
assert result["review_command_found"] is True
assert result["advisory_agent_found"] is True
# May have INFO issue about missing contract version (not an error/warning)
error_or_warning = [i for i in result["issues"]
if i["severity"].value in ("error", "warning")]
assert len(error_or_warning) == 0
@pytest.mark.asyncio
async def test_validate_workflow_integration_missing_gate(validation_tools, domain_plugin_missing_gate):
"""Test plugin missing gate command returns invalid with ERROR"""
result = await validation_tools.validate_workflow_integration(
domain_plugin_missing_gate,
"Domain/Data"
)
assert "error" not in result
assert result["valid"] is False
assert result["gate_command_found"] is False
assert result["review_command_found"] is True
assert result["advisory_agent_found"] is True
# Should have one ERROR for missing gate
error_issues = [i for i in result["issues"] if i["severity"].value == "error"]
assert len(error_issues) == 1
assert "gate" in error_issues[0]["message"].lower()
@pytest.mark.asyncio
async def test_validate_workflow_integration_minimal(validation_tools, domain_plugin_minimal):
"""Test minimal plugin returns invalid with multiple issues"""
result = await validation_tools.validate_workflow_integration(
domain_plugin_minimal,
"Domain/Test"
)
assert "error" not in result
assert result["valid"] is False
assert result["gate_command_found"] is False
assert result["review_command_found"] is False
assert result["advisory_agent_found"] is False
# Should have one ERROR (gate) and two WARNINGs (review, agent)
error_issues = [i for i in result["issues"] if i["severity"].value == "error"]
warning_issues = [i for i in result["issues"] if i["severity"].value == "warning"]
assert len(error_issues) == 1
assert len(warning_issues) == 2
@pytest.mark.asyncio
async def test_validate_workflow_integration_nonexistent_plugin(validation_tools, tmp_path):
"""Test error when plugin directory doesn't exist"""
result = await validation_tools.validate_workflow_integration(
str(tmp_path / "nonexistent"),
"Domain/Test"
)
assert "error" in result
assert "not found" in result["error"].lower()
# --- Gate Contract Version Tests ---
@pytest.fixture
def domain_plugin_with_contract(tmp_path):
"""Create domain plugin with gate_contract: v1 in frontmatter"""
plugin_dir = tmp_path / "viz-platform-versioned"
plugin_dir.mkdir()
(plugin_dir / ".claude-plugin").mkdir()
(plugin_dir / "commands").mkdir()
(plugin_dir / "agents").mkdir()
# Gate command with gate_contract in frontmatter
gate_cmd = plugin_dir / "commands" / "design-gate.md"
gate_cmd.write_text("""---
description: Design system compliance gate (pass/fail)
gate_contract: v1
---
# /design-gate
Binary pass/fail validation gate for design system compliance.
## Output
- **PASS**: All design system checks passed
- **FAIL**: Design system violations detected
""")
# Review command
review_cmd = plugin_dir / "commands" / "design-review.md"
review_cmd.write_text("""# /design-review
Comprehensive design system audit.
""")
# Advisory agent
agent = plugin_dir / "agents" / "design-reviewer.md"
agent.write_text("""# design-reviewer
Design system compliance auditor for Domain/Viz issues.
""")
return str(plugin_dir)
@pytest.mark.asyncio
async def test_validate_workflow_contract_match(validation_tools, domain_plugin_with_contract):
"""Test that matching expected_contract produces no warning"""
result = await validation_tools.validate_workflow_integration(
domain_plugin_with_contract,
"Domain/Viz",
expected_contract="v1"
)
assert "error" not in result
assert result["valid"] is True
assert result["gate_contract"] == "v1"
# Should have no warnings about contract mismatch
warning_issues = [i for i in result["issues"] if i["severity"].value == "warning"]
contract_warnings = [i for i in warning_issues if "contract" in i["message"].lower()]
assert len(contract_warnings) == 0
@pytest.mark.asyncio
async def test_validate_workflow_contract_mismatch(validation_tools, domain_plugin_with_contract):
"""Test that mismatched expected_contract produces WARNING"""
result = await validation_tools.validate_workflow_integration(
domain_plugin_with_contract,
"Domain/Viz",
expected_contract="v2" # Gate has v1
)
assert "error" not in result
assert result["valid"] is True # Contract mismatch doesn't affect validity
assert result["gate_contract"] == "v1"
# Should have warning about contract mismatch
warning_issues = [i for i in result["issues"] if i["severity"].value == "warning"]
contract_warnings = [i for i in warning_issues if "contract" in i["message"].lower()]
assert len(contract_warnings) == 1
assert "mismatch" in contract_warnings[0]["message"].lower()
assert "v1" in contract_warnings[0]["message"]
assert "v2" in contract_warnings[0]["message"]
@pytest.mark.asyncio
async def test_validate_workflow_no_contract(validation_tools, domain_plugin_complete):
"""Test that missing gate_contract produces INFO suggestion"""
result = await validation_tools.validate_workflow_integration(
domain_plugin_complete,
"Domain/Viz"
)
assert "error" not in result
assert result["valid"] is True
assert result["gate_contract"] is None
# Should have info issue about missing contract
info_issues = [i for i in result["issues"] if i["severity"].value == "info"]
contract_info = [i for i in info_issues if "contract" in i["message"].lower()]
assert len(contract_info) == 1
assert "does not declare" in contract_info[0]["message"].lower()

View File

@@ -330,7 +330,7 @@ class PandasTools:
return {'error': f'DataFrame not found: {data_ref}'}
try:
filtered = df.query(condition).reset_index(drop=True)
filtered = df.query(condition)
result_name = name or f"{data_ref}_filtered"
return self._check_and_store(
filtered,

View File

@@ -1,21 +0,0 @@
#!/bin/bash
# Capture original working directory before any cd operations
# This should be the user's project directory when launched by Claude Code
export CLAUDE_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CACHE_VENV="$HOME/.cache/claude-mcp-venvs/leo-claude-mktplace/data-platform/.venv"
LOCAL_VENV="$SCRIPT_DIR/.venv"
if [[ -f "$CACHE_VENV/bin/python" ]]; then
PYTHON="$CACHE_VENV/bin/python"
elif [[ -f "$LOCAL_VENV/bin/python" ]]; then
PYTHON="$LOCAL_VENV/bin/python"
else
echo "ERROR: No venv found. Run: ./scripts/setup-venvs.sh" >&2
exit 1
fi
cd "$SCRIPT_DIR"
export PYTHONPATH="$SCRIPT_DIR"
exec "$PYTHON" -m mcp_server.server "$@"

View File

@@ -1,6 +0,0 @@
2026-02-03T14:09:25 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/gitea/tests/test_config.py | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-02-03T14:09:33 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/gitea/tests/test_gitea_client.py | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-02-03T14:10:22 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/gitea/tests/test_issues.py | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-02-03T14:17:12 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/gitea/README.md | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-02-03T14:18:27 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/gitea/CHANGELOG.md | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-02-03T14:18:41 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/gitea/TESTING.md | docs/COMMANDS-CHEATSHEET.md CLAUDE.md

View File

@@ -1,92 +0,0 @@
# Changelog
All notable changes to the Gitea MCP Server will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [1.3.0] - 2026-02-03
### Added
- Pull request tools (7 tools):
- `list_pull_requests` - List PRs from repository
- `get_pull_request` - Get specific PR details
- `get_pr_diff` - Get PR diff content
- `get_pr_comments` - Get comments on a PR
- `create_pr_review` - Create PR review (approve/request changes/comment)
- `add_pr_comment` - Add comment to PR
- `create_pull_request` - Create new pull request
- Label creation tools (3 tools):
- `create_label` - Create repo-level label
- `create_org_label` - Create organization-level label
- `create_label_smart` - Auto-detect org vs repo for label creation
- Validation tools (2 tools):
- `validate_repo_org` - Check if repo belongs to organization
- `get_branch_protection` - Get branch protection rules
### Changed
- Total tools increased from 20 to 36
- Updated test suite to 64 tests (was 42)
### Fixed
- Test fixtures updated to use `owner/repo` format
- Fixed aggregate_issues tests to pass required `org` argument
## [1.2.0] - 2026-01-28
### Added
- Milestone management tools (5 tools):
- `list_milestones` - List all milestones
- `get_milestone` - Get specific milestone
- `create_milestone` - Create new milestone
- `update_milestone` - Update existing milestone
- `delete_milestone` - Delete a milestone
- Issue dependency tools (4 tools):
- `list_issue_dependencies` - List blocking issues
- `create_issue_dependency` - Create dependency between issues
- `remove_issue_dependency` - Remove dependency
- `get_execution_order` - Calculate parallelizable execution order
## [1.1.0] - 2026-01-21
### Added
- Wiki and lessons learned tools (7 tools):
- `list_wiki_pages` - List all wiki pages
- `get_wiki_page` - Get specific wiki page content
- `create_wiki_page` - Create new wiki page
- `update_wiki_page` - Update existing wiki page
- `create_lesson` - Create lessons learned entry
- `search_lessons` - Search lessons by query/tags
- `allocate_rfc_number` - Get next available RFC number
- Automatic git remote URL detection for repository configuration
- Support for SSH, HTTPS, and HTTP git URL formats
### Changed
- Configuration now uses `owner/repo` format exclusively
- Removed separate `GITEA_OWNER` configuration (now derived from repo path)
## [1.0.0] - 2025-01-06
### Added
- Initial release with 8 core tools:
- `list_issues` - List issues from repository
- `get_issue` - Get specific issue details
- `create_issue` - Create new issue with labels
- `update_issue` - Update existing issue
- `add_comment` - Add comment to issue
- `get_labels` - Get all labels (org + repo)
- `suggest_labels` - Intelligent label suggestion
- `aggregate_issues` - Cross-repository issue aggregation (PMO mode)
- Hybrid configuration system (system + project level)
- Branch-aware security model
- Mode detection (project vs company/PMO)
- 42 unit tests with mocks
- Comprehensive documentation
[Unreleased]: https://github.com/owner/repo/compare/v1.3.0...HEAD
[1.3.0]: https://github.com/owner/repo/compare/v1.2.0...v1.3.0
[1.2.0]: https://github.com/owner/repo/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/owner/repo/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/owner/repo/releases/tag/v1.0.0

View File

@@ -19,9 +19,8 @@ The Gitea MCP Server provides Claude Code with direct access to Gitea for issue
- **Hybrid Configuration**: System-level credentials + project-level paths
- **PMO Support**: Multi-repository aggregation for organization-wide views
### Tools Provided (36 total)
### Tools Provided
#### Issue Management (6 tools)
| Tool | Description | Mode |
|------|-------------|------|
| `list_issues` | List issues from repository | Both |
@@ -29,61 +28,9 @@ The Gitea MCP Server provides Claude Code with direct access to Gitea for issue
| `create_issue` | Create new issue with labels | Both |
| `update_issue` | Update existing issue | Both |
| `add_comment` | Add comment to issue | Both |
| `aggregate_issues` | Cross-repository issue aggregation | PMO Only |
#### Label Management (5 tools)
| Tool | Description | Mode |
|------|-------------|------|
| `get_labels` | Get all labels (org + repo) | Both |
| `suggest_labels` | Intelligent label suggestion | Both |
| `create_label` | Create repo-level label | Both |
| `create_org_label` | Create organization-level label | Both |
| `create_label_smart` | Auto-detect org vs repo for label creation | Both |
#### Wiki & Lessons Learned (7 tools)
| Tool | Description | Mode |
|------|-------------|------|
| `list_wiki_pages` | List all wiki pages | Both |
| `get_wiki_page` | Get specific wiki page content | Both |
| `create_wiki_page` | Create new wiki page | Both |
| `update_wiki_page` | Update existing wiki page | Both |
| `create_lesson` | Create lessons learned entry | Both |
| `search_lessons` | Search lessons by query/tags | Both |
| `allocate_rfc_number` | Get next available RFC number | Both |
#### Milestone Management (5 tools)
| Tool | Description | Mode |
|------|-------------|------|
| `list_milestones` | List all milestones | Both |
| `get_milestone` | Get specific milestone | Both |
| `create_milestone` | Create new milestone | Both |
| `update_milestone` | Update existing milestone | Both |
| `delete_milestone` | Delete a milestone | Both |
#### Issue Dependencies (4 tools)
| Tool | Description | Mode |
|------|-------------|------|
| `list_issue_dependencies` | List blocking issues | Both |
| `create_issue_dependency` | Create dependency between issues | Both |
| `remove_issue_dependency` | Remove dependency | Both |
| `get_execution_order` | Calculate parallelizable execution order | Both |
#### Pull Request Tools (7 tools)
| Tool | Description | Mode |
|------|-------------|------|
| `list_pull_requests` | List PRs from repository | Both |
| `get_pull_request` | Get specific PR details | Both |
| `get_pr_diff` | Get PR diff content | Both |
| `get_pr_comments` | Get comments on a PR | Both |
| `create_pr_review` | Create PR review (approve/request changes) | Both |
| `add_pr_comment` | Add comment to PR | Both |
| `create_pull_request` | Create new pull request | Both |
#### Validation Tools (2 tools)
| Tool | Description | Mode |
|------|-------------|------|
| `validate_repo_org` | Check if repo belongs to organization | Both |
| `get_branch_protection` | Get branch protection rules | Both |
| `aggregate_issues` | Cross-repository issue aggregation | PMO Only |
## Architecture
@@ -93,20 +40,15 @@ The Gitea MCP Server provides Claude Code with direct access to Gitea for issue
mcp-servers/gitea/
├── .venv/ # Python virtual environment
├── requirements.txt # Python dependencies
├── run.sh # Entry point script
├── mcp_server/
│ ├── __init__.py
│ ├── server.py # MCP server entry point (36 tools)
│ ├── config.py # Configuration loader with auto-detection
│ ├── server.py # MCP server entry point
│ ├── config.py # Configuration loader
│ ├── gitea_client.py # Gitea API client
│ └── tools/
│ ├── __init__.py
│ ├── issues.py # Issue management tools
── labels.py # Label management tools
│ ├── wiki.py # Wiki & lessons learned tools
│ ├── milestones.py # Milestone management tools
│ ├── dependencies.py # Issue dependency tools
│ └── pull_requests.py # Pull request tools
│ ├── issues.py # Issue tools
── labels.py # Label tools
├── tests/
│ ├── __init__.py
│ ├── test_config.py
@@ -114,8 +56,7 @@ mcp-servers/gitea/
│ ├── test_issues.py
│ └── test_labels.py
├── README.md # This file
── TESTING.md # Testing instructions
└── CHANGELOG.md # Version history
── TESTING.md # Testing instructions
```
### Mode Detection
@@ -170,6 +111,7 @@ mkdir -p ~/.config/claude
cat > ~/.config/claude/gitea.env << EOF
GITEA_API_URL=https://gitea.example.com/api/v1
GITEA_API_TOKEN=your_gitea_token_here
GITEA_OWNER=bandit
EOF
chmod 600 ~/.config/claude/gitea.env
@@ -195,34 +137,14 @@ For company/PMO mode, omit the `.env` file or don't set `GITEA_REPO`.
**Required Variables**:
- `GITEA_API_URL` - Gitea API endpoint (e.g., `https://gitea.example.com/api/v1`)
- `GITEA_API_TOKEN` - Personal access token with repo permissions
- `GITEA_OWNER` - Organization or user name (e.g., `bandit`)
### Project-Level Configuration
**File**: `<project-root>/.env`
**Optional Variables**:
- `GITEA_REPO` - Repository in `owner/repo` format (enables project mode)
### Automatic Repository Detection
If `GITEA_REPO` is not set, the server auto-detects the repository from your git remote:
**Supported URL Formats**:
- SSH: `ssh://git@gitea.example.com:22/owner/repo.git`
- SSH short: `git@gitea.example.com:owner/repo.git`
- HTTPS: `https://gitea.example.com/owner/repo.git`
- HTTP: `http://gitea.example.com/owner/repo.git`
The repository is extracted as `owner/repo` format automatically.
### Project Directory Detection
The server finds your project directory using these strategies (in order):
1. `CLAUDE_PROJECT_DIR` environment variable (highest priority)
2. `PWD` environment variable (if `.git` or `.env` present)
3. Current working directory (if `.git` or `.env` present)
4. Falls back to company/PMO mode if no project found
- `GITEA_REPO` - Repository name (enables project mode)
### Generating Gitea API Token
@@ -298,13 +220,13 @@ suggestions = await label_tools.suggest_labels(context)
### Unit Tests
Run all 64 unit tests with mocks:
Run all 42 unit tests with mocks:
```bash
pytest tests/ -v
```
Expected: `64 passed`
Expected: `42 passed in 0.57s`
### Integration Tests
@@ -405,15 +327,11 @@ See [TESTING.md](./TESTING.md#troubleshooting) for more details.
### Project Structure
- `config.py` - Hybrid configuration loader with auto-detection
- `config.py` - Hybrid configuration loader with mode detection
- `gitea_client.py` - Synchronous Gitea API client using requests
- `tools/issues.py` - Issue management with branch detection
- `tools/labels.py` - Label management and intelligent suggestions
- `tools/wiki.py` - Wiki pages and lessons learned
- `tools/milestones.py` - Milestone CRUD operations
- `tools/dependencies.py` - Issue dependency tracking
- `tools/pull_requests.py` - PR review and management
- `server.py` - MCP server with 36 tools over JSON-RPC 2.0 stdio
- `tools/issues.py` - Async wrappers with branch detection
- `tools/labels.py` - Label management and suggestion
- `server.py` - MCP server with JSON-RPC 2.0 over stdio
### Adding New Tools
@@ -456,14 +374,18 @@ def list_issues(self, state='open', labels=None, repo=None):
## Changelog
See [CHANGELOG.md](./CHANGELOG.md) for full version history.
### v1.0.0 (2025-01-06) - Phase 1 Complete
### Recent Updates
- **v1.3.0** - Pull request tools (7 tools), label creation tools (3)
- **v1.2.0** - Milestone management (5 tools), issue dependencies (4 tools)
- **v1.1.0** - Wiki & lessons learned system (7 tools)
- **v1.0.0** - Initial release with core issue/label tools (8 tools)
✅ Initial implementation:
- Configuration management (hybrid system + project)
- Gitea API client with all CRUD operations
- MCP server with 8 tools
- Issue tools with branch detection
- Label tools with intelligent suggestions
- Mode detection (project vs company)
- Branch-aware security model
- 42 unit tests (100% passing)
- Comprehensive documentation
## License
@@ -485,6 +407,6 @@ For issues or questions:
---
**Built for**: Leo Claude Marketplace - Project Management Plugins
**Tools**: 36
**Phase**: 1 (Complete)
**Status**: ✅ Production Ready
**Last Updated**: 2026-02-03
**Last Updated**: 2025-01-06

View File

@@ -28,7 +28,7 @@ source .venv/bin/activate # Linux/Mac
### Running All Tests
Run all 64 unit tests:
Run all 42 unit tests:
```bash
pytest tests/ -v
@@ -36,7 +36,7 @@ pytest tests/ -v
Expected output:
```
============================== 64 passed ==============================
============================== 42 passed in 0.57s ==============================
```
### Running Specific Test Files
@@ -532,7 +532,7 @@ python -m mcp_server.server
After completing all tests, verify:
- ✅ All 64 unit tests pass
- ✅ All 42 unit tests pass
- ✅ MCP server starts without errors
- ✅ Configuration loads correctly
- ✅ Gitea API client connects successfully
@@ -548,7 +548,7 @@ After completing all tests, verify:
Phase 1 is complete when:
1. **All unit tests pass** (64/64)
1. **All unit tests pass** (42/42)
2. **MCP server starts without errors**
3. **Can list issues from Gitea**
4. **Can create issues with labels** (in development mode)

View File

@@ -1,30 +0,0 @@
"""
Gitea MCP Server package.
Provides MCP tools for Gitea integration via JSON-RPC 2.0.
For external consumers (e.g., HTTP transport), use:
from mcp_server import get_tool_definitions, create_tool_dispatcher, GiteaClient
# Get tool schemas
tools = get_tool_definitions()
# Create dispatcher bound to a client
client = GiteaClient()
dispatch = create_tool_dispatcher(client)
result = await dispatch("list_issues", {"state": "open"})
"""
__version__ = "1.0.0"
from .tool_registry import get_tool_definitions, create_tool_dispatcher
from .gitea_client import GiteaClient
from .config import GiteaConfig
__all__ = [
"__version__",
"get_tool_definitions",
"create_tool_dispatcher",
"GiteaClient",
"GiteaConfig",
]

View File

@@ -53,7 +53,6 @@ class GiteaClient:
self,
state: str = 'open',
labels: Optional[List[str]] = None,
milestone: Optional[str] = None,
repo: Optional[str] = None
) -> List[Dict]:
"""
@@ -62,7 +61,6 @@ class GiteaClient:
Args:
state: Issue state (open, closed, all)
labels: Filter by labels
milestone: Filter by milestone title (exact match)
repo: Repository in 'owner/repo' format
Returns:
@@ -73,8 +71,6 @@ class GiteaClient:
params = {'state': state}
if labels:
params['labels'] = ','.join(labels)
if milestone:
params['milestones'] = milestone
logger.info(f"Listing issues from {owner}/{target_repo} with state={state}")
response = self.session.get(url, params=params)
response.raise_for_status()
@@ -139,24 +135,9 @@ class GiteaClient:
body: Optional[str] = None,
state: Optional[str] = None,
labels: Optional[List[str]] = None,
milestone: Optional[int] = None,
repo: Optional[str] = None
) -> Dict:
"""
Update existing issue.
Args:
issue_number: Issue number to update
title: New title (optional)
body: New body (optional)
state: New state - 'open' or 'closed' (optional)
labels: New labels (optional)
milestone: Milestone ID to assign (optional)
repo: Repository in 'owner/repo' format
Returns:
Updated issue dictionary
"""
"""Update existing issue. Repo must be 'owner/repo' format."""
owner, target_repo = self._parse_repo(repo)
url = f"{self.base_url}/repos/{owner}/{target_repo}/issues/{issue_number}"
data = {}
@@ -168,8 +149,6 @@ class GiteaClient:
data['state'] = state
if labels is not None:
data['labels'] = labels
if milestone is not None:
data['milestone'] = milestone
logger.info(f"Updating issue #{issue_number} in {owner}/{target_repo}")
response = self.session.patch(url, json=data)
response.raise_for_status()
@@ -260,11 +239,8 @@ class GiteaClient:
repo: Optional[str] = None
) -> Dict:
"""Get a specific wiki page by name."""
from urllib.parse import quote
owner, target_repo = self._parse_repo(repo)
# URL-encode the page_name to handle special characters like ':'
encoded_page_name = quote(page_name, safe='')
url = f"{self.base_url}/repos/{owner}/{target_repo}/wiki/page/{encoded_page_name}"
url = f"{self.base_url}/repos/{owner}/{target_repo}/wiki/page/{page_name}"
logger.info(f"Getting wiki page '{page_name}' from {owner}/{target_repo}")
response = self.session.get(url)
response.raise_for_status()
@@ -295,13 +271,9 @@ class GiteaClient:
repo: Optional[str] = None
) -> Dict:
"""Update an existing wiki page."""
from urllib.parse import quote
owner, target_repo = self._parse_repo(repo)
# URL-encode the page_name to handle special characters like ':'
encoded_page_name = quote(page_name, safe='')
url = f"{self.base_url}/repos/{owner}/{target_repo}/wiki/page/{encoded_page_name}"
url = f"{self.base_url}/repos/{owner}/{target_repo}/wiki/page/{page_name}"
data = {
'title': page_name, # CRITICAL: include title to preserve page name
'content_base64': self._encode_base64(content)
}
logger.info(f"Updating wiki page '{page_name}' in {owner}/{target_repo}")
@@ -315,11 +287,8 @@ class GiteaClient:
repo: Optional[str] = None
) -> bool:
"""Delete a wiki page."""
from urllib.parse import quote
owner, target_repo = self._parse_repo(repo)
# URL-encode the page_name to handle special characters like ':'
encoded_page_name = quote(page_name, safe='')
url = f"{self.base_url}/repos/{owner}/{target_repo}/wiki/page/{encoded_page_name}"
url = f"{self.base_url}/repos/{owner}/{target_repo}/wiki/page/{page_name}"
logger.info(f"Deleting wiki page '{page_name}' from {owner}/{target_repo}")
response = self.session.delete(url)
response.raise_for_status()
@@ -808,42 +777,3 @@ class GiteaClient:
response = self.session.post(url, json=data)
response.raise_for_status()
return response.json()
def create_pull_request(
self,
title: str,
body: str,
head: str,
base: str,
labels: Optional[List[str]] = None,
repo: Optional[str] = None
) -> Dict:
"""
Create a new pull request.
Args:
title: PR title
body: PR description/body
head: Source branch name (the branch with changes)
base: Target branch name (the branch to merge into)
labels: Optional list of label names
repo: Repository in 'owner/repo' format
Returns:
Created pull request dictionary
"""
owner, target_repo = self._parse_repo(repo)
url = f"{self.base_url}/repos/{owner}/{target_repo}/pulls"
data = {
'title': title,
'body': body,
'head': head,
'base': base
}
if labels:
label_ids = self._resolve_label_ids(labels, owner, target_repo)
data['labels'] = label_ids
logger.info(f"Creating PR '{title}' in {owner}/{target_repo}: {head} -> {base}")
response = self.session.post(url, json=data)
response.raise_for_status()
return response.json()

View File

@@ -5,13 +5,19 @@ Provides Gitea tools to Claude Code via JSON-RPC 2.0 over stdio.
"""
import asyncio
import logging
import json
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
from .config import GiteaConfig
from .gitea_client import GiteaClient
from .tool_registry import get_tool_definitions, create_tool_dispatcher
from .tools.issues import IssueTools
from .tools.labels import LabelTools
from .tools.wiki import WikiTools
from .tools.milestones import MilestoneTools
from .tools.dependencies import DependencyTools
from .tools.pull_requests import PullRequestTools
# Suppress noisy MCP validation warnings on stderr
logging.basicConfig(level=logging.INFO)
@@ -27,7 +33,12 @@ class GiteaMCPServer:
self.server = Server("gitea-mcp")
self.config = None
self.client = None
self._dispatcher = None
self.issue_tools = None
self.label_tools = None
self.wiki_tools = None
self.milestone_tools = None
self.dependency_tools = None
self.pr_tools = None
async def initialize(self):
"""
@@ -41,7 +52,12 @@ class GiteaMCPServer:
self.config = config_loader.load()
self.client = GiteaClient()
self._dispatcher = create_tool_dispatcher(self.client)
self.issue_tools = IssueTools(self.client)
self.label_tools = LabelTools(self.client)
self.wiki_tools = WikiTools(self.client)
self.milestone_tools = MilestoneTools(self.client)
self.dependency_tools = DependencyTools(self.client)
self.pr_tools = PullRequestTools(self.client)
logger.info(f"Gitea MCP Server initialized in {self.config['mode']} mode")
except Exception as e:
@@ -54,7 +70,782 @@ class GiteaMCPServer:
@self.server.list_tools()
async def list_tools() -> list[Tool]:
"""Return list of available tools"""
return get_tool_definitions()
return [
Tool(
name="list_issues",
description="List issues from Gitea repository",
inputSchema={
"type": "object",
"properties": {
"state": {
"type": "string",
"enum": ["open", "closed", "all"],
"default": "open",
"description": "Issue state filter"
},
"labels": {
"type": "array",
"items": {"type": "string"},
"description": "Filter by labels"
},
"repo": {
"type": "string",
"description": "Repository name (for PMO mode)"
}
}
}
),
Tool(
name="get_issue",
description="Get specific issue details",
inputSchema={
"type": "object",
"properties": {
"issue_number": {
"type": "integer",
"description": "Issue number"
},
"repo": {
"type": "string",
"description": "Repository name (for PMO mode)"
}
},
"required": ["issue_number"]
}
),
Tool(
name="create_issue",
description="Create a new issue in Gitea",
inputSchema={
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Issue title"
},
"body": {
"type": "string",
"description": "Issue description"
},
"labels": {
"type": "array",
"items": {"type": "string"},
"description": "List of label names"
},
"repo": {
"type": "string",
"description": "Repository name (for PMO mode)"
}
},
"required": ["title", "body"]
}
),
Tool(
name="update_issue",
description="Update existing issue",
inputSchema={
"type": "object",
"properties": {
"issue_number": {
"type": "integer",
"description": "Issue number"
},
"title": {
"type": "string",
"description": "New title"
},
"body": {
"type": "string",
"description": "New body"
},
"state": {
"type": "string",
"enum": ["open", "closed"],
"description": "New state"
},
"labels": {
"type": "array",
"items": {"type": "string"},
"description": "New labels"
},
"repo": {
"type": "string",
"description": "Repository name (for PMO mode)"
}
},
"required": ["issue_number"]
}
),
Tool(
name="add_comment",
description="Add comment to issue",
inputSchema={
"type": "object",
"properties": {
"issue_number": {
"type": "integer",
"description": "Issue number"
},
"comment": {
"type": "string",
"description": "Comment text"
},
"repo": {
"type": "string",
"description": "Repository name (for PMO mode)"
}
},
"required": ["issue_number", "comment"]
}
),
Tool(
name="get_labels",
description="Get all available labels (org + repo)",
inputSchema={
"type": "object",
"properties": {
"repo": {
"type": "string",
"description": "Repository name (for PMO mode)"
}
}
}
),
Tool(
name="suggest_labels",
description="Analyze context and suggest appropriate labels",
inputSchema={
"type": "object",
"properties": {
"context": {
"type": "string",
"description": "Issue title + description or sprint context"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
},
"required": ["context"]
}
),
Tool(
name="aggregate_issues",
description="Fetch issues across all repositories (PMO mode)",
inputSchema={
"type": "object",
"properties": {
"org": {
"type": "string",
"description": "Organization name (e.g. 'bandit')"
},
"state": {
"type": "string",
"enum": ["open", "closed", "all"],
"default": "open",
"description": "Issue state filter"
},
"labels": {
"type": "array",
"items": {"type": "string"},
"description": "Filter by labels"
}
},
"required": ["org"]
}
),
# Wiki Tools (Lessons Learned)
Tool(
name="list_wiki_pages",
description="List all wiki pages in repository",
inputSchema={
"type": "object",
"properties": {
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
}
}
),
Tool(
name="get_wiki_page",
description="Get a specific wiki page by name",
inputSchema={
"type": "object",
"properties": {
"page_name": {
"type": "string",
"description": "Wiki page name/path"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
},
"required": ["page_name"]
}
),
Tool(
name="create_wiki_page",
description="Create a new wiki page",
inputSchema={
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Page title/name"
},
"content": {
"type": "string",
"description": "Page content (markdown)"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
},
"required": ["title", "content"]
}
),
Tool(
name="update_wiki_page",
description="Update an existing wiki page",
inputSchema={
"type": "object",
"properties": {
"page_name": {
"type": "string",
"description": "Wiki page name/path"
},
"content": {
"type": "string",
"description": "New page content (markdown)"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
},
"required": ["page_name", "content"]
}
),
Tool(
name="create_lesson",
description="Create a lessons learned entry in the wiki",
inputSchema={
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Lesson title (e.g., 'Sprint 16 - Prevent Infinite Loops')"
},
"content": {
"type": "string",
"description": "Lesson content (markdown with context, problem, solution, prevention)"
},
"tags": {
"type": "array",
"items": {"type": "string"},
"description": "Tags for categorization"
},
"category": {
"type": "string",
"default": "sprints",
"description": "Category (sprints, patterns, architecture, etc.)"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
},
"required": ["title", "content", "tags"]
}
),
Tool(
name="search_lessons",
description="Search lessons learned from previous sprints",
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query (optional)"
},
"tags": {
"type": "array",
"items": {"type": "string"},
"description": "Tags to filter by (optional)"
},
"limit": {
"type": "integer",
"default": 20,
"description": "Maximum results"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
}
}
),
# Milestone Tools
Tool(
name="list_milestones",
description="List all milestones in repository",
inputSchema={
"type": "object",
"properties": {
"state": {
"type": "string",
"enum": ["open", "closed", "all"],
"default": "open",
"description": "Milestone state filter"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
}
}
),
Tool(
name="get_milestone",
description="Get a specific milestone by ID",
inputSchema={
"type": "object",
"properties": {
"milestone_id": {
"type": "integer",
"description": "Milestone ID"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
},
"required": ["milestone_id"]
}
),
Tool(
name="create_milestone",
description="Create a new milestone",
inputSchema={
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Milestone title"
},
"description": {
"type": "string",
"description": "Milestone description"
},
"due_on": {
"type": "string",
"description": "Due date (ISO 8601 format)"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
},
"required": ["title"]
}
),
Tool(
name="update_milestone",
description="Update an existing milestone",
inputSchema={
"type": "object",
"properties": {
"milestone_id": {
"type": "integer",
"description": "Milestone ID"
},
"title": {
"type": "string",
"description": "New title"
},
"description": {
"type": "string",
"description": "New description"
},
"state": {
"type": "string",
"enum": ["open", "closed"],
"description": "New state"
},
"due_on": {
"type": "string",
"description": "New due date"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
},
"required": ["milestone_id"]
}
),
Tool(
name="delete_milestone",
description="Delete a milestone",
inputSchema={
"type": "object",
"properties": {
"milestone_id": {
"type": "integer",
"description": "Milestone ID"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
},
"required": ["milestone_id"]
}
),
# Dependency Tools
Tool(
name="list_issue_dependencies",
description="List all dependencies for an issue (issues that block this one)",
inputSchema={
"type": "object",
"properties": {
"issue_number": {
"type": "integer",
"description": "Issue number"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
},
"required": ["issue_number"]
}
),
Tool(
name="create_issue_dependency",
description="Create a dependency (issue depends on another issue)",
inputSchema={
"type": "object",
"properties": {
"issue_number": {
"type": "integer",
"description": "Issue that will depend on another"
},
"depends_on": {
"type": "integer",
"description": "Issue that blocks issue_number"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
},
"required": ["issue_number", "depends_on"]
}
),
Tool(
name="remove_issue_dependency",
description="Remove a dependency between issues",
inputSchema={
"type": "object",
"properties": {
"issue_number": {
"type": "integer",
"description": "Issue that depends on another"
},
"depends_on": {
"type": "integer",
"description": "Issue being depended on"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
},
"required": ["issue_number", "depends_on"]
}
),
Tool(
name="get_execution_order",
description="Get parallelizable execution order for issues based on dependencies",
inputSchema={
"type": "object",
"properties": {
"issue_numbers": {
"type": "array",
"items": {"type": "integer"},
"description": "List of issue numbers to analyze"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
},
"required": ["issue_numbers"]
}
),
# Validation Tools
Tool(
name="validate_repo_org",
description="Check if repository belongs to an organization",
inputSchema={
"type": "object",
"properties": {
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
}
}
),
Tool(
name="get_branch_protection",
description="Get branch protection rules",
inputSchema={
"type": "object",
"properties": {
"branch": {
"type": "string",
"description": "Branch name"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
},
"required": ["branch"]
}
),
Tool(
name="create_label",
description="Create a new label in the repository (for repo-specific labels like Component/*, Tech/*)",
inputSchema={
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Label name (e.g., 'Component/Backend', 'Tech/Python')"
},
"color": {
"type": "string",
"description": "Label color (hex code)"
},
"description": {
"type": "string",
"description": "Label description"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
},
"required": ["name", "color"]
}
),
Tool(
name="create_org_label",
description="Create a new label at organization level (for workflow labels like Type/*, Priority/*, Complexity/*, Effort/*)",
inputSchema={
"type": "object",
"properties": {
"org": {
"type": "string",
"description": "Organization name"
},
"name": {
"type": "string",
"description": "Label name (e.g., 'Type/Bug', 'Priority/High')"
},
"color": {
"type": "string",
"description": "Label color (hex code)"
},
"description": {
"type": "string",
"description": "Label description"
}
},
"required": ["org", "name", "color"]
}
),
Tool(
name="create_label_smart",
description="Create a label at the appropriate level (org or repo) based on category. Org: Type/*, Priority/*, Complexity/*, Effort/*, Risk/*, Source/*, Agent/*. Repo: Component/*, Tech/*",
inputSchema={
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Label name (e.g., 'Type/Bug', 'Component/Backend')"
},
"color": {
"type": "string",
"description": "Label color (hex code)"
},
"description": {
"type": "string",
"description": "Label description"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
},
"required": ["name", "color"]
}
),
# Pull Request Tools
Tool(
name="list_pull_requests",
description="List pull requests from repository",
inputSchema={
"type": "object",
"properties": {
"state": {
"type": "string",
"enum": ["open", "closed", "all"],
"default": "open",
"description": "PR state filter"
},
"sort": {
"type": "string",
"enum": ["oldest", "recentupdate", "leastupdate", "mostcomment", "leastcomment", "priority"],
"default": "recentupdate",
"description": "Sort order"
},
"labels": {
"type": "array",
"items": {"type": "string"},
"description": "Filter by labels"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
}
}
),
Tool(
name="get_pull_request",
description="Get specific pull request details",
inputSchema={
"type": "object",
"properties": {
"pr_number": {
"type": "integer",
"description": "Pull request number"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
},
"required": ["pr_number"]
}
),
Tool(
name="get_pr_diff",
description="Get the diff for a pull request",
inputSchema={
"type": "object",
"properties": {
"pr_number": {
"type": "integer",
"description": "Pull request number"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
},
"required": ["pr_number"]
}
),
Tool(
name="get_pr_comments",
description="Get comments on a pull request",
inputSchema={
"type": "object",
"properties": {
"pr_number": {
"type": "integer",
"description": "Pull request number"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
},
"required": ["pr_number"]
}
),
Tool(
name="create_pr_review",
description="Create a review on a pull request (approve, request changes, or comment)",
inputSchema={
"type": "object",
"properties": {
"pr_number": {
"type": "integer",
"description": "Pull request number"
},
"body": {
"type": "string",
"description": "Review body/summary"
},
"event": {
"type": "string",
"enum": ["APPROVE", "REQUEST_CHANGES", "COMMENT"],
"default": "COMMENT",
"description": "Review action"
},
"comments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"path": {"type": "string"},
"position": {"type": "integer"},
"body": {"type": "string"}
}
},
"description": "Optional inline comments"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
},
"required": ["pr_number", "body"]
}
),
Tool(
name="add_pr_comment",
description="Add a general comment to a pull request",
inputSchema={
"type": "object",
"properties": {
"pr_number": {
"type": "integer",
"description": "Pull request number"
},
"body": {
"type": "string",
"description": "Comment text"
},
"repo": {
"type": "string",
"description": "Repository name (owner/repo format)"
}
},
"required": ["pr_number", "body"]
}
)
]
@self.server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
@@ -68,7 +859,120 @@ class GiteaMCPServer:
Returns:
List of TextContent with results
"""
return await self._dispatcher(name, arguments)
try:
# Route to appropriate tool handler
if name == "list_issues":
result = await self.issue_tools.list_issues(**arguments)
elif name == "get_issue":
result = await self.issue_tools.get_issue(**arguments)
elif name == "create_issue":
result = await self.issue_tools.create_issue(**arguments)
elif name == "update_issue":
result = await self.issue_tools.update_issue(**arguments)
elif name == "add_comment":
result = await self.issue_tools.add_comment(**arguments)
elif name == "get_labels":
result = await self.label_tools.get_labels(**arguments)
elif name == "suggest_labels":
result = await self.label_tools.suggest_labels(**arguments)
elif name == "aggregate_issues":
result = await self.issue_tools.aggregate_issues(**arguments)
# Wiki tools
elif name == "list_wiki_pages":
result = await self.wiki_tools.list_wiki_pages(**arguments)
elif name == "get_wiki_page":
result = await self.wiki_tools.get_wiki_page(**arguments)
elif name == "create_wiki_page":
result = await self.wiki_tools.create_wiki_page(**arguments)
elif name == "update_wiki_page":
result = await self.wiki_tools.update_wiki_page(**arguments)
elif name == "create_lesson":
result = await self.wiki_tools.create_lesson(**arguments)
elif name == "search_lessons":
tags = arguments.get('tags')
result = await self.wiki_tools.search_lessons(
query=arguments.get('query'),
tags=tags,
limit=arguments.get('limit', 20),
repo=arguments.get('repo')
)
# Milestone tools
elif name == "list_milestones":
result = await self.milestone_tools.list_milestones(**arguments)
elif name == "get_milestone":
result = await self.milestone_tools.get_milestone(**arguments)
elif name == "create_milestone":
result = await self.milestone_tools.create_milestone(**arguments)
elif name == "update_milestone":
result = await self.milestone_tools.update_milestone(**arguments)
elif name == "delete_milestone":
result = await self.milestone_tools.delete_milestone(**arguments)
# Dependency tools
elif name == "list_issue_dependencies":
result = await self.dependency_tools.list_issue_dependencies(**arguments)
elif name == "create_issue_dependency":
result = await self.dependency_tools.create_issue_dependency(**arguments)
elif name == "remove_issue_dependency":
result = await self.dependency_tools.remove_issue_dependency(**arguments)
elif name == "get_execution_order":
result = await self.dependency_tools.get_execution_order(**arguments)
# Validation tools
elif name == "validate_repo_org":
is_org = self.client.is_org_repo(arguments.get('repo'))
result = {'is_organization': is_org}
elif name == "get_branch_protection":
result = self.client.get_branch_protection(
arguments['branch'],
arguments.get('repo')
)
elif name == "create_label":
result = self.client.create_label(
arguments['name'],
arguments['color'],
arguments.get('description'),
arguments.get('repo')
)
elif name == "create_org_label":
result = self.client.create_org_label(
arguments['org'],
arguments['name'],
arguments['color'],
arguments.get('description')
)
elif name == "create_label_smart":
result = await self.label_tools.create_label_smart(
arguments['name'],
arguments['color'],
arguments.get('description'),
arguments.get('repo')
)
# Pull Request tools
elif name == "list_pull_requests":
result = await self.pr_tools.list_pull_requests(**arguments)
elif name == "get_pull_request":
result = await self.pr_tools.get_pull_request(**arguments)
elif name == "get_pr_diff":
result = await self.pr_tools.get_pr_diff(**arguments)
elif name == "get_pr_comments":
result = await self.pr_tools.get_pr_comments(**arguments)
elif name == "create_pr_review":
result = await self.pr_tools.create_pr_review(**arguments)
elif name == "add_pr_comment":
result = await self.pr_tools.add_pr_comment(**arguments)
else:
raise ValueError(f"Unknown tool: {name}")
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
except Exception as e:
logger.error(f"Tool {name} failed: {e}")
return [TextContent(
type="text",
text=f"Error: {str(e)}"
)]
async def run(self):
"""Run the MCP server"""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,6 @@ Provides async wrappers for issue CRUD operations with:
- Comprehensive error handling
"""
import asyncio
import os
import subprocess
import logging
from typing import List, Dict, Optional
@@ -28,34 +27,19 @@ class IssueTools:
"""
self.gitea = gitea_client
def _get_project_directory(self) -> Optional[str]:
"""
Get the user's project directory from environment.
Returns:
Project directory path or None if not set
"""
return os.environ.get('CLAUDE_PROJECT_DIR')
def _get_current_branch(self) -> str:
"""
Get current git branch from user's project directory.
Uses CLAUDE_PROJECT_DIR environment variable to determine the correct
directory for git operations, avoiding the bug where git runs from
the installed plugin directory instead of the user's project.
Get current git branch.
Returns:
Current branch name or 'unknown' if not in a git repo
"""
try:
project_dir = self._get_project_directory()
result = subprocess.run(
['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
capture_output=True,
text=True,
check=True,
cwd=project_dir # Run git in project directory, not plugin directory
check=True
)
return result.stdout.strip()
except subprocess.CalledProcessError:
@@ -82,13 +66,7 @@ class IssueTools:
return operation in ['list_issues', 'get_issue', 'get_labels', 'create_issue']
# Development branches (full access)
# Include all common feature/fix branch patterns
dev_prefixes = (
'feat/', 'feature/', 'dev/',
'fix/', 'bugfix/', 'hotfix/',
'chore/', 'refactor/', 'docs/', 'test/'
)
if branch in ['development', 'develop'] or branch.startswith(dev_prefixes):
if branch in ['development', 'develop'] or branch.startswith(('feat/', 'feature/', 'dev/')):
return True
# Unknown branch - be restrictive
@@ -98,7 +76,6 @@ class IssueTools:
self,
state: str = 'open',
labels: Optional[List[str]] = None,
milestone: Optional[str] = None,
repo: Optional[str] = None
) -> List[Dict]:
"""
@@ -107,7 +84,6 @@ class IssueTools:
Args:
state: Issue state (open, closed, all)
labels: Filter by labels
milestone: Filter by milestone title (exact match)
repo: Override configured repo (for PMO multi-repo)
Returns:
@@ -126,7 +102,7 @@ class IssueTools:
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
None,
lambda: self.gitea.list_issues(state, labels, milestone, repo)
lambda: self.gitea.list_issues(state, labels, repo)
)
async def get_issue(
@@ -202,7 +178,6 @@ class IssueTools:
body: Optional[str] = None,
state: Optional[str] = None,
labels: Optional[List[str]] = None,
milestone: Optional[int] = None,
repo: Optional[str] = None
) -> Dict:
"""
@@ -214,7 +189,6 @@ class IssueTools:
body: New body (optional)
state: New state - 'open' or 'closed' (optional)
labels: New labels (optional)
milestone: Milestone ID to assign (optional)
repo: Override configured repo (for PMO multi-repo)
Returns:
@@ -233,7 +207,7 @@ class IssueTools:
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
None,
lambda: self.gitea.update_issue(issue_number, title, body, state, labels, milestone, repo)
lambda: self.gitea.update_issue(issue_number, title, body, state, labels, repo)
)
async def add_comment(

View File

@@ -7,7 +7,6 @@ Provides async wrappers for PR operations with:
- Comprehensive error handling
"""
import asyncio
import os
import subprocess
import logging
from typing import List, Dict, Optional
@@ -28,34 +27,19 @@ class PullRequestTools:
"""
self.gitea = gitea_client
def _get_project_directory(self) -> Optional[str]:
"""
Get the user's project directory from environment.
Returns:
Project directory path or None if not set
"""
return os.environ.get('CLAUDE_PROJECT_DIR')
def _get_current_branch(self) -> str:
"""
Get current git branch from user's project directory.
Uses CLAUDE_PROJECT_DIR environment variable to determine the correct
directory for git operations, avoiding the bug where git runs from
the installed plugin directory instead of the user's project.
Get current git branch.
Returns:
Current branch name or 'unknown' if not in a git repo
"""
try:
project_dir = self._get_project_directory()
result = subprocess.run(
['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
capture_output=True,
text=True,
check=True,
cwd=project_dir # Run git in project directory, not plugin directory
check=True
)
return result.stdout.strip()
except subprocess.CalledProcessError:
@@ -85,13 +69,7 @@ class PullRequestTools:
return operation in read_ops + ['add_pr_comment']
# Development branches (full access)
# Include all common feature/fix branch patterns
dev_prefixes = (
'feat/', 'feature/', 'dev/',
'fix/', 'bugfix/', 'hotfix/',
'chore/', 'refactor/', 'docs/', 'test/'
)
if branch in ['development', 'develop'] or branch.startswith(dev_prefixes):
if branch in ['development', 'develop'] or branch.startswith(('feat/', 'feature/', 'dev/')):
return True
# Unknown branch - be restrictive
@@ -294,42 +272,3 @@ class PullRequestTools:
None,
lambda: self.gitea.add_pr_comment(pr_number, body, repo)
)
async def create_pull_request(
self,
title: str,
body: str,
head: str,
base: str,
labels: Optional[List[str]] = None,
repo: Optional[str] = None
) -> Dict:
"""
Create a new pull request (async wrapper with branch check).
Args:
title: PR title
body: PR description/body
head: Source branch name (the branch with changes)
base: Target branch name (the branch to merge into)
labels: Optional list of label names
repo: Override configured repo (for PMO multi-repo)
Returns:
Created pull request dictionary
Raises:
PermissionError: If operation not allowed on current branch
"""
if not self._check_branch_permissions('create_pull_request'):
branch = self._get_current_branch()
raise PermissionError(
f"Cannot create PR on branch '{branch}'. "
f"Switch to a development or feature branch to create PRs."
)
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
None,
lambda: self.gitea.create_pull_request(title, body, head, base, labels, repo)
)

View File

@@ -4,11 +4,9 @@ Wiki management tools for MCP server.
Provides async wrappers for wiki operations to support lessons learned:
- Page CRUD operations
- Lessons learned creation and search
- RFC number allocation
"""
import asyncio
import logging
import re
from typing import List, Dict, Optional
logging.basicConfig(level=logging.INFO)
@@ -149,39 +147,3 @@ class WikiTools:
lambda: self.gitea.search_lessons(query, tags, repo)
)
return results[:limit]
async def allocate_rfc_number(self, repo: Optional[str] = None) -> Dict:
"""
Allocate the next available RFC number.
Scans existing wiki pages for RFC-NNNN pattern and returns
the next sequential number.
Args:
repo: Repository in owner/repo format
Returns:
Dict with 'next_number' (int) and 'formatted' (str like 'RFC-0001')
"""
pages = await self.list_wiki_pages(repo)
# Extract RFC numbers from page titles
rfc_numbers = []
rfc_pattern = re.compile(r'^RFC-(\d{4})')
for page in pages:
title = page.get('title', '')
match = rfc_pattern.match(title)
if match:
rfc_numbers.append(int(match.group(1)))
# Calculate next number
if rfc_numbers:
next_num = max(rfc_numbers) + 1
else:
next_num = 1
return {
'next_number': next_num,
'formatted': f'RFC-{next_num:04d}'
}

View File

@@ -1,43 +0,0 @@
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "gitea-mcp-server"
version = "1.0.0"
description = "MCP Server for Gitea integration - provides issue, label, wiki, milestone, dependency, and PR tools"
readme = "README.md"
requires-python = ">=3.10"
license = {text = "MIT"}
authors = [
{ name = "Leo Miranda" }
]
keywords = ["mcp", "gitea", "claude", "tools"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"mcp>=0.9.0",
"python-dotenv>=1.0.0",
"requests>=2.31.0",
"pydantic>=2.5.0",
]
[project.optional-dependencies]
test = [
"pytest>=7.4.3",
"pytest-asyncio>=0.23.0",
]
[tool.setuptools.packages.find]
where = ["."]
include = ["mcp_server*"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

View File

@@ -1,21 +0,0 @@
#!/bin/bash
# Capture original working directory before any cd operations
# This should be the user's project directory when launched by Claude Code
export CLAUDE_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CACHE_VENV="$HOME/.cache/claude-mcp-venvs/leo-claude-mktplace/gitea/.venv"
LOCAL_VENV="$SCRIPT_DIR/.venv"
if [[ -f "$CACHE_VENV/bin/python" ]]; then
PYTHON="$CACHE_VENV/bin/python"
elif [[ -f "$LOCAL_VENV/bin/python" ]]; then
PYTHON="$LOCAL_VENV/bin/python"
else
echo "ERROR: No venv found. Run: ./scripts/setup-venvs.sh" >&2
exit 1
fi
cd "$SCRIPT_DIR"
export PYTHONPATH="$SCRIPT_DIR"
exec "$PYTHON" -m mcp_server.server "$@"

View File

@@ -28,6 +28,7 @@ def test_load_system_config(tmp_path, monkeypatch):
assert result['api_url'] == 'https://test.com/api/v1'
assert result['api_token'] == 'test_token'
assert result['owner'] == 'test_owner'
assert result['mode'] == 'company' # No repo specified
assert result['repo'] is None

View File

@@ -14,7 +14,8 @@ def mock_config():
mock_instance.load.return_value = {
'api_url': 'https://test.com/api/v1',
'api_token': 'test_token',
'repo': 'test_owner/test_repo', # Combined owner/repo format
'owner': 'test_owner',
'repo': 'test_repo',
'mode': 'project'
}
yield mock_cfg
@@ -30,7 +31,8 @@ def test_client_initialization(gitea_client):
"""Test client initializes with correct configuration"""
assert gitea_client.base_url == 'https://test.com/api/v1'
assert gitea_client.token == 'test_token'
assert gitea_client.repo == 'test_owner/test_repo' # Combined format
assert gitea_client.owner == 'test_owner'
assert gitea_client.repo == 'test_repo'
assert gitea_client.mode == 'project'
assert 'Authorization' in gitea_client.session.headers
assert gitea_client.session.headers['Authorization'] == 'token test_token'
@@ -90,20 +92,15 @@ def test_create_issue(gitea_client):
}
mock_response.raise_for_status = Mock()
# Mock is_org_repo to avoid network call during label resolution
with patch.object(gitea_client, 'is_org_repo', return_value=True):
# Mock get_org_labels and get_labels for label resolution
with patch.object(gitea_client, 'get_org_labels', return_value=[{'name': 'Type/Bug', 'id': 1}]):
with patch.object(gitea_client, 'get_labels', return_value=[]):
with patch.object(gitea_client.session, 'post', return_value=mock_response):
issue = gitea_client.create_issue(
title='New Issue',
body='Issue body',
labels=['Type/Bug']
)
with patch.object(gitea_client.session, 'post', return_value=mock_response):
issue = gitea_client.create_issue(
title='New Issue',
body='Issue body',
labels=['Type/Bug']
)
assert issue['title'] == 'New Issue'
gitea_client.session.post.assert_called_once()
assert issue['title'] == 'New Issue'
gitea_client.session.post.assert_called_once()
def test_update_issue(gitea_client):
@@ -164,7 +161,7 @@ def test_get_org_labels(gitea_client):
mock_response.raise_for_status = Mock()
with patch.object(gitea_client.session, 'get', return_value=mock_response):
labels = gitea_client.get_org_labels(org='test_owner')
labels = gitea_client.get_org_labels()
assert len(labels) == 2
@@ -179,7 +176,7 @@ def test_list_repos(gitea_client):
mock_response.raise_for_status = Mock()
with patch.object(gitea_client.session, 'get', return_value=mock_response):
repos = gitea_client.list_repos(org='test_owner')
repos = gitea_client.list_repos()
assert len(repos) == 2
assert repos[0]['name'] == 'repo1'
@@ -199,7 +196,7 @@ def test_aggregate_issues(gitea_client):
[{'number': 2, 'title': 'Issue 2'}] # repo2
])
aggregated = gitea_client.aggregate_issues(org='test_owner', state='open')
aggregated = gitea_client.aggregate_issues(state='open')
assert 'repo1' in aggregated
assert 'repo2' in aggregated
@@ -208,13 +205,14 @@ def test_aggregate_issues(gitea_client):
def test_no_repo_specified_error(gitea_client):
"""Test error when repository not specified or invalid format"""
"""Test error when repository not specified"""
# Create client without repo
with patch('mcp_server.gitea_client.GiteaConfig') as mock_cfg:
mock_instance = mock_cfg.return_value
mock_instance.load.return_value = {
'api_url': 'https://test.com/api/v1',
'api_token': 'test_token',
'owner': 'test_owner',
'repo': None, # No repo
'mode': 'company'
}
@@ -223,7 +221,7 @@ def test_no_repo_specified_error(gitea_client):
with pytest.raises(ValueError) as exc_info:
client.list_issues()
assert "Use 'owner/repo' format" in str(exc_info.value)
assert "Repository not specified" in str(exc_info.value)
# ========================================

View File

@@ -119,26 +119,22 @@ async def test_aggregate_issues_company_mode(issue_tools):
'repo2': [{'number': 2}]
})
aggregated = await issue_tools.aggregate_issues(org='test_owner')
aggregated = await issue_tools.aggregate_issues()
assert 'repo1' in aggregated
assert 'repo2' in aggregated
@pytest.mark.asyncio
async def test_aggregate_issues_project_mode(issue_tools):
"""Test that aggregate_issues works in project mode with org argument"""
async def test_aggregate_issues_project_mode_error(issue_tools):
"""Test that aggregate_issues fails in project mode"""
issue_tools.gitea.mode = 'project'
with patch.object(issue_tools, '_get_current_branch', return_value='development'):
issue_tools.gitea.aggregate_issues = Mock(return_value={
'repo1': [{'number': 1}]
})
with pytest.raises(ValueError) as exc_info:
await issue_tools.aggregate_issues()
# aggregate_issues now works in any mode when org is provided
aggregated = await issue_tools.aggregate_issues(org='test_owner')
assert 'repo1' in aggregated
assert "only available in company mode" in str(exc_info.value)
def test_branch_detection():

View File

@@ -79,69 +79,6 @@ Add to your Claude Code MCP configuration (`~/.config/claude/mcp.json` or projec
1. **System-level** (`~/.config/claude/netbox.env`): Credentials and defaults
2. **Project-level** (`.env` in current directory): Optional overrides
## Module Filtering (Token Optimization)
By default, the NetBox MCP server registers all 182 tools across 8 modules, consuming ~19,810 tokens of context. For most workflows, you only need a subset of modules.
### Configuration
Add `NETBOX_ENABLED_MODULES` to your `~/.config/claude/netbox.env`:
```bash
# Enable only specific modules (comma-separated)
NETBOX_ENABLED_MODULES=dcim,ipam,virtualization,extras
```
If unset, all modules are enabled (backward compatible).
### Available Modules
| Module | Tool Count | Description | cmdb-assistant Commands |
|--------|------------|-------------|------------------------|
| `dcim` | ~60 | Sites, devices, racks, interfaces, cables | `/cmdb-device`, `/cmdb-site`, `/cmdb-search`, `/cmdb-topology` |
| `ipam` | ~40 | IP addresses, prefixes, VLANs, VRFs | `/cmdb-ip`, `/ip-conflicts`, `/cmdb-search` |
| `virtualization` | ~20 | Clusters, VMs, VM interfaces | `/cmdb-search`, `/cmdb-audit`, `/cmdb-register` |
| `extras` | ~12 | Tags, journal entries, audit log | `/change-audit`, `/cmdb-register` |
| `circuits` | ~15 | Providers, circuits, terminations | — |
| `tenancy` | ~12 | Tenants, contacts | — |
| `vpn` | ~15 | Tunnels, IKE/IPSec policies, L2VPN | — |
| `wireless` | ~8 | Wireless LANs, links, groups | — |
### Recommended Configurations
**For cmdb-assistant users** (~43 tools, ~4,500 tokens):
```bash
NETBOX_ENABLED_MODULES=dcim,ipam,virtualization,extras
```
**Basic infrastructure** (~100 tools):
```bash
NETBOX_ENABLED_MODULES=dcim,ipam
```
**Full CMDB** (all modules, ~182 tools):
```bash
# Omit NETBOX_ENABLED_MODULES or set to all modules
NETBOX_ENABLED_MODULES=dcim,ipam,circuits,virtualization,tenancy,vpn,wireless,extras
```
### Startup Logging
On startup, the server logs enabled modules and tool count:
```
NetBox MCP Server initialized: 43 tools registered (modules: dcim, extras, ipam, virtualization)
```
### Disabled Tool Behavior
Calling a tool from a disabled module returns a clear error:
```
Tool 'circuits_list_circuits' is not available (module 'circuits' not enabled).
Enabled modules: dcim, extras, ipam, virtualization
```
## Available Tools
### DCIM (Data Center Infrastructure Management)
@@ -191,18 +128,18 @@ Enabled modules: dcim, extras, ipam, virtualization
| `circuits_create_provider` | Create a provider |
| `circuits_list_circuits` | List circuits |
| `circuits_create_circuit` | Create a circuit |
| `circ_list_terminations` | List terminations |
| `circuits_list_circuit_terminations` | List terminations |
| ... and more |
### Virtualization
| Tool | Description |
|------|-------------|
| `virt_list_clusters` | List clusters |
| `virt_create_cluster` | Create a cluster |
| `virt_list_vms` | List VMs |
| `virt_create_vm` | Create a VM |
| `virt_list_vm_ifaces` | List VM interfaces |
| `virtualization_list_clusters` | List clusters |
| `virtualization_create_cluster` | Create a cluster |
| `virtualization_list_virtual_machines` | List VMs |
| `virtualization_create_virtual_machine` | Create a VM |
| `virtualization_list_vm_interfaces` | List VM interfaces |
| ... and more |
### Tenancy
@@ -230,9 +167,9 @@ Enabled modules: dcim, extras, ipam, virtualization
| Tool | Description |
|------|-------------|
| `wlan_list_lans` | List wireless LANs |
| `wlan_create_lan` | Create a WLAN |
| `wlan_list_links` | List wireless links |
| `wireless_list_wireless_lans` | List wireless LANs |
| `wireless_create_wireless_lan` | Create a WLAN |
| `wireless_list_wireless_links` | List wireless links |
| ... and more |
### Extras

View File

@@ -9,17 +9,11 @@ from pathlib import Path
from dotenv import load_dotenv
import os
import logging
from typing import Dict, List, Optional, Set
from typing import Dict, Optional
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# All available NetBox modules
ALL_MODULES = frozenset([
'dcim', 'ipam', 'circuits', 'virtualization',
'tenancy', 'vpn', 'wireless', 'extras'
])
class NetBoxConfig:
"""Configuration loader for NetBox MCP Server"""
@@ -29,7 +23,6 @@ class NetBoxConfig:
self.api_token: Optional[str] = None
self.verify_ssl: bool = True
self.timeout: int = 30
self.enabled_modules: Set[str] = set(ALL_MODULES)
def load(self) -> Dict[str, any]:
"""
@@ -80,9 +73,6 @@ class NetBoxConfig:
self.timeout = 30
logger.warning(f"Invalid NETBOX_TIMEOUT value '{timeout_str}', using default 30")
# Module filtering
self.enabled_modules = self._load_enabled_modules()
# Validate required variables
self._validate()
@@ -94,8 +84,7 @@ class NetBoxConfig:
'api_url': self.api_url,
'api_token': self.api_token,
'verify_ssl': self.verify_ssl,
'timeout': self.timeout,
'enabled_modules': self.enabled_modules
'timeout': self.timeout
}
def _validate(self) -> None:
@@ -117,40 +106,3 @@ class NetBoxConfig:
f"Missing required configuration: {', '.join(missing)}\n"
"Check your ~/.config/claude/netbox.env file"
)
def _load_enabled_modules(self) -> Set[str]:
"""
Load enabled modules from NETBOX_ENABLED_MODULES environment variable.
Format: Comma-separated list of module names.
Example: NETBOX_ENABLED_MODULES=dcim,ipam,virtualization,extras
Returns:
Set of enabled module names. If env var is unset/empty, returns all modules.
"""
modules_str = os.getenv('NETBOX_ENABLED_MODULES', '').strip()
if not modules_str:
logger.info("NETBOX_ENABLED_MODULES not set, all modules enabled (default)")
return set(ALL_MODULES)
# Parse comma-separated list, strip whitespace
requested = {m.strip().lower() for m in modules_str.split(',') if m.strip()}
# Validate module names
invalid = requested - ALL_MODULES
if invalid:
logger.warning(
f"Unknown modules in NETBOX_ENABLED_MODULES: {', '.join(sorted(invalid))}. "
f"Valid modules: {', '.join(sorted(ALL_MODULES))}"
)
# Return only valid modules
enabled = requested & ALL_MODULES
if not enabled:
logger.warning("No valid modules enabled, falling back to all modules")
return set(ALL_MODULES)
logger.info(f"Enabled modules: {', '.join(sorted(enabled))}")
return enabled

View File

@@ -8,12 +8,11 @@ Tenancy, VPN, Wireless, and Extras.
import asyncio
import logging
import json
from typing import Optional, Set
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
from .config import NetBoxConfig, ALL_MODULES
from .config import NetBoxConfig
from .netbox_client import NetBoxClient
from .tools.dcim import DCIMTools
from .tools.ipam import IPAMTools
@@ -1454,49 +1453,6 @@ TOOL_NAME_MAP = {
}
# Map tool name prefixes to module names.
# This handles both full prefixes and shortened prefixes used in TOOL_NAME_MAP.
PREFIX_TO_MODULE = {
'dcim': 'dcim',
'ipam': 'ipam',
'circuits': 'circuits',
'circ': 'circuits', # Shortened prefix
'virtualization': 'virtualization',
'virt': 'virtualization', # Shortened prefix
'tenancy': 'tenancy',
'vpn': 'vpn',
'wireless': 'wireless',
'wlan': 'wireless', # Shortened prefix
'extras': 'extras',
}
def _get_tool_module(tool_name: str) -> Optional[str]:
"""
Determine which module a tool belongs to.
Checks TOOL_NAME_MAP first for shortened names, then falls back to prefix extraction.
Args:
tool_name: The tool name (e.g., 'dcim_list_devices', 'virt_list_vms')
Returns:
Module name (e.g., 'dcim', 'virtualization') or None if unknown
"""
# Check mapped short names first
if tool_name in TOOL_NAME_MAP:
category, _ = TOOL_NAME_MAP[tool_name]
return category
# Fall back to prefix extraction
parts = tool_name.split('_', 1)
if len(parts) < 2:
return None
prefix = parts[0]
return PREFIX_TO_MODULE.get(prefix)
class NetBoxMCPServer:
"""MCP Server for NetBox integration"""
@@ -1504,8 +1460,6 @@ class NetBoxMCPServer:
self.server = Server("netbox-mcp")
self.config = None
self.client = None
self.enabled_modules: Set[str] = set(ALL_MODULES)
# Tool instances - only instantiated for enabled modules
self.dcim_tools = None
self.ipam_tools = None
self.circuits_tools = None
@@ -1520,39 +1474,18 @@ class NetBoxMCPServer:
try:
config_loader = NetBoxConfig()
self.config = config_loader.load()
self.enabled_modules = self.config['enabled_modules']
self.client = NetBoxClient()
self.dcim_tools = DCIMTools(self.client)
self.ipam_tools = IPAMTools(self.client)
self.circuits_tools = CircuitsTools(self.client)
self.virtualization_tools = VirtualizationTools(self.client)
self.tenancy_tools = TenancyTools(self.client)
self.vpn_tools = VPNTools(self.client)
self.wireless_tools = WirelessTools(self.client)
self.extras_tools = ExtrasTools(self.client)
# Conditionally instantiate tool classes for enabled modules only
if 'dcim' in self.enabled_modules:
self.dcim_tools = DCIMTools(self.client)
if 'ipam' in self.enabled_modules:
self.ipam_tools = IPAMTools(self.client)
if 'circuits' in self.enabled_modules:
self.circuits_tools = CircuitsTools(self.client)
if 'virtualization' in self.enabled_modules:
self.virtualization_tools = VirtualizationTools(self.client)
if 'tenancy' in self.enabled_modules:
self.tenancy_tools = TenancyTools(self.client)
if 'vpn' in self.enabled_modules:
self.vpn_tools = VPNTools(self.client)
if 'wireless' in self.enabled_modules:
self.wireless_tools = WirelessTools(self.client)
if 'extras' in self.enabled_modules:
self.extras_tools = ExtrasTools(self.client)
# Count tools that will be registered
tool_count = sum(
1 for name in TOOL_DEFINITIONS
if _get_tool_module(name) in self.enabled_modules
)
modules_str = ', '.join(sorted(self.enabled_modules))
logger.info(
f"NetBox MCP Server initialized: {tool_count} tools registered "
f"(modules: {modules_str})"
)
logger.info(f"NetBox MCP Server initialized for {self.config['api_url']}")
except Exception as e:
logger.error(f"Failed to initialize: {e}")
raise
@@ -1562,14 +1495,9 @@ class NetBoxMCPServer:
@self.server.list_tools()
async def list_tools() -> list[Tool]:
"""Return list of available tools, filtered by enabled modules"""
"""Return list of available tools"""
tools = []
for name, definition in TOOL_DEFINITIONS.items():
# Filter tools by enabled modules
module = _get_tool_module(name)
if module not in self.enabled_modules:
continue
tools.append(Tool(
name=name,
description=definition['description'],
@@ -1604,14 +1532,6 @@ class NetBoxMCPServer:
'virtualization_list_virtual_machines') to meet the 28-character
limit. TOOL_NAME_MAP handles the translation to actual method names.
"""
# Check module is enabled (routing guard)
module = _get_tool_module(name)
if module and module not in self.enabled_modules:
raise ValueError(
f"Tool '{name}' is not available (module '{module}' not enabled). "
f"Enabled modules: {', '.join(sorted(self.enabled_modules))}"
)
# Check if this is a mapped short name
if name in TOOL_NAME_MAP:
category, method_name = TOOL_NAME_MAP[name]

View File

@@ -1,21 +0,0 @@
#!/bin/bash
# Capture original working directory before any cd operations
# This should be the user's project directory when launched by Claude Code
export CLAUDE_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CACHE_VENV="$HOME/.cache/claude-mcp-venvs/leo-claude-mktplace/netbox/.venv"
LOCAL_VENV="$SCRIPT_DIR/.venv"
if [[ -f "$CACHE_VENV/bin/python" ]]; then
PYTHON="$CACHE_VENV/bin/python"
elif [[ -f "$LOCAL_VENV/bin/python" ]]; then
PYTHON="$LOCAL_VENV/bin/python"
else
echo "ERROR: No venv found. Run: ./scripts/setup-venvs.sh" >&2
exit 1
fi
cd "$SCRIPT_DIR"
export PYTHONPATH="$SCRIPT_DIR"
exec "$PYTHON" -m mcp_server.server "$@"

View File

@@ -1,5 +0,0 @@
2026-01-26T11:40:11 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/viz-platform/registry/dmc_2_5.json | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-01-26T13:46:31 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/viz-platform/tests/test_chart_tools.py | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-01-26T13:46:32 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/viz-platform/tests/test_theme_tools.py | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-01-26T13:46:34 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/viz-platform/tests/test_theme_tools.py | docs/COMMANDS-CHEATSHEET.md CLAUDE.md
2026-01-26T13:46:35 | mcp-servers | /home/lmiranda/claude-plugins-work/mcp-servers/viz-platform/tests/test_theme_tools.py | docs/COMMANDS-CHEATSHEET.md CLAUDE.md

View File

@@ -1,115 +0,0 @@
# viz-platform MCP Server
Model Context Protocol (MCP) server for Dash Mantine Components validation and visualization tools.
## Overview
This MCP server provides 21 tools for:
- **DMC Validation**: Version-locked component registry prevents Claude from hallucinating invalid props
- **Chart Creation**: Plotly-based visualization with theme integration
- **Layout Composition**: Dashboard layouts with responsive grids
- **Theme Management**: Design token-based theming system
- **Page Structure**: Multi-page Dash app generation
## Tools
### DMC Tools (3)
| Tool | Description |
|------|-------------|
| `list_components` | List available DMC components by category |
| `get_component_props` | Get valid props, types, and defaults for a component |
| `validate_component` | Validate component definition before use |
### Chart Tools (2)
| Tool | Description |
|------|-------------|
| `chart_create` | Create Plotly chart (line, bar, scatter, pie, histogram, area, heatmap) |
| `chart_configure_interaction` | Configure chart interactions (zoom, pan, hover) |
### Layout Tools (5)
| Tool | Description |
|------|-------------|
| `layout_create` | Create dashboard layout structure |
| `layout_add_filter` | Add filter components to layout |
| `layout_set_grid` | Configure responsive grid settings |
| `layout_get` | Retrieve layout configuration |
| `layout_add_section` | Add sections to layout |
### Theme Tools (6)
| Tool | Description |
|------|-------------|
| `theme_create` | Create new theme with design tokens |
| `theme_extend` | Extend existing theme with overrides |
| `theme_validate` | Validate theme completeness |
| `theme_export_css` | Export theme as CSS custom properties |
| `theme_list` | List available themes |
| `theme_activate` | Set active theme for visualizations |
### Page Tools (5)
| Tool | Description |
|------|-------------|
| `page_create` | Create new page structure |
| `page_add_navbar` | Add navigation bar to page |
| `page_set_auth` | Configure page authentication |
| `page_list` | List available pages |
| `page_get_app_config` | Get full app configuration |
## Configuration
### Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `DMC_VERSION` | No | Dash Mantine Components version (auto-detected if installed) |
| `VIZ_DEFAULT_THEME` | No | Default theme name |
| `CLAUDE_PROJECT_DIR` | No | Project directory for theme storage |
### Theme Storage
Themes can be stored at two levels:
- **User-level**: `~/.config/claude/themes/`
- **Project-level**: `{project}/.viz-platform/themes/`
Project-level themes take precedence.
## Component Registry
The server uses a static JSON registry for DMC component validation:
- Pre-generated from DMC source code
- Version-tagged (e.g., `dmc_2_5.json`)
- Prevents hallucination of non-existent props
- Fast, deterministic validation
Registry files are stored in `registry/` directory.
## Tests
94 tests with coverage:
- `test_config.py`: 82% coverage
- `test_component_registry.py`: 92% coverage
- `test_dmc_tools.py`: 88% coverage
- `test_chart_tools.py`: 68% coverage
- `test_theme_tools.py`: 99% coverage
Run tests:
```bash
cd mcp-servers/viz-platform
source .venv/bin/activate
pytest tests/ -v
```
## Dependencies
- Python 3.10+
- FastMCP
- plotly
- dash-mantine-components (optional, for version detection)
## Usage
This MCP server is used by the `viz-platform` plugin. See the plugin's commands in `plugins/viz-platform/commands/` for usage.

View File

@@ -1,7 +0,0 @@
"""
viz-platform MCP Server package.
Provides Dash Mantine Components validation and visualization tools to Claude Code.
"""
__version__ = "1.0.0"

View File

@@ -1,479 +0,0 @@
"""
Accessibility validation tools for color blindness and WCAG compliance.
Provides tools for validating color palettes against color blindness
simulations and WCAG contrast requirements.
"""
import logging
import math
from typing import Dict, List, Optional, Any, Tuple
logger = logging.getLogger(__name__)
# Color-blind safe palettes
SAFE_PALETTES = {
"categorical": {
"name": "Paul Tol's Qualitative",
"colors": ["#4477AA", "#EE6677", "#228833", "#CCBB44", "#66CCEE", "#AA3377", "#BBBBBB"],
"description": "Distinguishable for all types of color blindness"
},
"ibm": {
"name": "IBM Design",
"colors": ["#648FFF", "#785EF0", "#DC267F", "#FE6100", "#FFB000"],
"description": "IBM's accessible color palette"
},
"okabe_ito": {
"name": "Okabe-Ito",
"colors": ["#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00", "#CC79A7", "#000000"],
"description": "Optimized for all color vision deficiencies"
},
"tableau_colorblind": {
"name": "Tableau Colorblind 10",
"colors": ["#006BA4", "#FF800E", "#ABABAB", "#595959", "#5F9ED1",
"#C85200", "#898989", "#A2C8EC", "#FFBC79", "#CFCFCF"],
"description": "Industry-standard accessible palette"
}
}
# Simulation matrices for color blindness (LMS color space transformation)
# These approximate how colors appear to people with different types of color blindness
SIMULATION_MATRICES = {
"deuteranopia": {
# Green-blind (most common)
"severity": "common",
"population": "6% males, 0.4% females",
"description": "Difficulty distinguishing red from green (green-blind)",
"matrix": [
[0.625, 0.375, 0.0],
[0.700, 0.300, 0.0],
[0.0, 0.300, 0.700]
]
},
"protanopia": {
# Red-blind
"severity": "common",
"population": "2.5% males, 0.05% females",
"description": "Difficulty distinguishing red from green (red-blind)",
"matrix": [
[0.567, 0.433, 0.0],
[0.558, 0.442, 0.0],
[0.0, 0.242, 0.758]
]
},
"tritanopia": {
# Blue-blind (rare)
"severity": "rare",
"population": "0.01% total",
"description": "Difficulty distinguishing blue from yellow",
"matrix": [
[0.950, 0.050, 0.0],
[0.0, 0.433, 0.567],
[0.0, 0.475, 0.525]
]
}
}
class AccessibilityTools:
"""
Color accessibility validation tools.
Validates colors for WCAG compliance and color blindness accessibility.
"""
def __init__(self, theme_store=None):
"""
Initialize accessibility tools.
Args:
theme_store: Optional ThemeStore for theme color extraction
"""
self.theme_store = theme_store
def _hex_to_rgb(self, hex_color: str) -> Tuple[int, int, int]:
"""Convert hex color to RGB tuple."""
hex_color = hex_color.lstrip('#')
if len(hex_color) == 3:
hex_color = ''.join([c * 2 for c in hex_color])
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
def _rgb_to_hex(self, rgb: Tuple[int, int, int]) -> str:
"""Convert RGB tuple to hex color."""
return '#{:02x}{:02x}{:02x}'.format(
max(0, min(255, int(rgb[0]))),
max(0, min(255, int(rgb[1]))),
max(0, min(255, int(rgb[2])))
)
def _get_relative_luminance(self, rgb: Tuple[int, int, int]) -> float:
"""
Calculate relative luminance per WCAG 2.1.
https://www.w3.org/WAI/GL/wiki/Relative_luminance
"""
def channel_luminance(value: int) -> float:
v = value / 255
return v / 12.92 if v <= 0.03928 else ((v + 0.055) / 1.055) ** 2.4
r, g, b = rgb
return (
0.2126 * channel_luminance(r) +
0.7152 * channel_luminance(g) +
0.0722 * channel_luminance(b)
)
def _get_contrast_ratio(self, color1: str, color2: str) -> float:
"""
Calculate contrast ratio between two colors per WCAG 2.1.
Returns ratio between 1:1 and 21:1.
"""
rgb1 = self._hex_to_rgb(color1)
rgb2 = self._hex_to_rgb(color2)
l1 = self._get_relative_luminance(rgb1)
l2 = self._get_relative_luminance(rgb2)
lighter = max(l1, l2)
darker = min(l1, l2)
return (lighter + 0.05) / (darker + 0.05)
def _simulate_color_blindness(
self,
hex_color: str,
deficiency_type: str
) -> str:
"""
Simulate how a color appears with a specific color blindness type.
Uses linear RGB transformation approximation.
"""
if deficiency_type not in SIMULATION_MATRICES:
return hex_color
rgb = self._hex_to_rgb(hex_color)
matrix = SIMULATION_MATRICES[deficiency_type]["matrix"]
# Apply transformation matrix
r = rgb[0] * matrix[0][0] + rgb[1] * matrix[0][1] + rgb[2] * matrix[0][2]
g = rgb[0] * matrix[1][0] + rgb[1] * matrix[1][1] + rgb[2] * matrix[1][2]
b = rgb[0] * matrix[2][0] + rgb[1] * matrix[2][1] + rgb[2] * matrix[2][2]
return self._rgb_to_hex((r, g, b))
def _get_color_distance(self, color1: str, color2: str) -> float:
"""
Calculate perceptual color distance (CIE76 approximation).
Returns a value where < 20 means colors may be hard to distinguish.
"""
rgb1 = self._hex_to_rgb(color1)
rgb2 = self._hex_to_rgb(color2)
# Simple Euclidean distance in RGB space (approximation)
# For production, should use CIEDE2000
return math.sqrt(
(rgb1[0] - rgb2[0]) ** 2 +
(rgb1[1] - rgb2[1]) ** 2 +
(rgb1[2] - rgb2[2]) ** 2
)
async def accessibility_validate_colors(
self,
colors: List[str],
check_types: Optional[List[str]] = None,
min_contrast_ratio: float = 4.5
) -> Dict[str, Any]:
"""
Validate a list of colors for accessibility.
Args:
colors: List of hex colors to validate
check_types: Color blindness types to check (default: all)
min_contrast_ratio: Minimum WCAG contrast ratio (default: 4.5 for AA)
Returns:
Dict with:
- issues: List of accessibility issues found
- simulations: How colors appear under each deficiency
- recommendations: Suggestions for improvement
- safe_palettes: Color-blind safe palette suggestions
"""
check_types = check_types or list(SIMULATION_MATRICES.keys())
issues = []
simulations = {}
# Normalize colors
normalized_colors = [c.upper() if c.startswith('#') else f'#{c.upper()}' for c in colors]
# Simulate each color blindness type
for deficiency in check_types:
if deficiency not in SIMULATION_MATRICES:
continue
simulated = [self._simulate_color_blindness(c, deficiency) for c in normalized_colors]
simulations[deficiency] = {
"original": normalized_colors,
"simulated": simulated,
"info": SIMULATION_MATRICES[deficiency]
}
# Check if any color pairs become indistinguishable
for i in range(len(normalized_colors)):
for j in range(i + 1, len(normalized_colors)):
distance = self._get_color_distance(simulated[i], simulated[j])
if distance < 30: # Threshold for distinguishability
issues.append({
"type": "distinguishability",
"severity": "warning" if distance > 15 else "error",
"colors": [normalized_colors[i], normalized_colors[j]],
"affected_by": [deficiency],
"simulated_colors": [simulated[i], simulated[j]],
"distance": round(distance, 1),
"message": f"Colors may be hard to distinguish for {deficiency} ({SIMULATION_MATRICES[deficiency]['description']})"
})
# Check contrast ratios against white and black backgrounds
for color in normalized_colors:
white_contrast = self._get_contrast_ratio(color, "#FFFFFF")
black_contrast = self._get_contrast_ratio(color, "#000000")
if white_contrast < min_contrast_ratio and black_contrast < min_contrast_ratio:
issues.append({
"type": "contrast_ratio",
"severity": "error",
"colors": [color],
"white_contrast": round(white_contrast, 2),
"black_contrast": round(black_contrast, 2),
"required": min_contrast_ratio,
"message": f"Insufficient contrast against both white ({white_contrast:.1f}:1) and black ({black_contrast:.1f}:1) backgrounds"
})
# Generate recommendations
recommendations = self._generate_recommendations(issues)
# Calculate overall score
error_count = sum(1 for i in issues if i["severity"] == "error")
warning_count = sum(1 for i in issues if i["severity"] == "warning")
if error_count == 0 and warning_count == 0:
score = "A"
elif error_count == 0 and warning_count <= 2:
score = "B"
elif error_count <= 2:
score = "C"
else:
score = "D"
return {
"colors_checked": normalized_colors,
"overall_score": score,
"issue_count": len(issues),
"issues": issues,
"simulations": simulations,
"recommendations": recommendations,
"safe_palettes": SAFE_PALETTES
}
async def accessibility_validate_theme(
self,
theme_name: str
) -> Dict[str, Any]:
"""
Validate a theme's colors for accessibility.
Args:
theme_name: Theme name to validate
Returns:
Dict with accessibility validation results
"""
if not self.theme_store:
return {
"error": "Theme store not configured",
"theme_name": theme_name
}
theme = self.theme_store.get_theme(theme_name)
if not theme:
available = self.theme_store.list_themes()
return {
"error": f"Theme '{theme_name}' not found. Available: {available}",
"theme_name": theme_name
}
# Extract colors from theme
colors = []
tokens = theme.get("tokens", {})
color_tokens = tokens.get("colors", {})
def extract_colors(obj, prefix=""):
"""Recursively extract color values."""
if isinstance(obj, str) and (obj.startswith('#') or len(obj) == 6):
colors.append(obj if obj.startswith('#') else f'#{obj}')
elif isinstance(obj, dict):
for key, value in obj.items():
extract_colors(value, f"{prefix}.{key}")
elif isinstance(obj, list):
for item in obj:
extract_colors(item, prefix)
extract_colors(color_tokens)
# Validate extracted colors
result = await self.accessibility_validate_colors(colors)
result["theme_name"] = theme_name
# Add theme-specific checks
primary = color_tokens.get("primary")
background = color_tokens.get("background", {})
text = color_tokens.get("text", {})
if primary and background:
bg_color = background.get("base") if isinstance(background, dict) else background
if bg_color:
contrast = self._get_contrast_ratio(primary, bg_color)
if contrast < 4.5:
result["issues"].append({
"type": "primary_contrast",
"severity": "error",
"colors": [primary, bg_color],
"ratio": round(contrast, 2),
"required": 4.5,
"message": f"Primary color has insufficient contrast ({contrast:.1f}:1) against background"
})
return result
async def accessibility_suggest_alternative(
self,
color: str,
deficiency_type: str
) -> Dict[str, Any]:
"""
Suggest accessible alternative colors.
Args:
color: Original hex color
deficiency_type: Type of color blindness to optimize for
Returns:
Dict with alternative color suggestions
"""
rgb = self._hex_to_rgb(color)
suggestions = []
# Suggest shifting hue while maintaining saturation and brightness
# For red-green deficiency, shift toward blue or yellow
if deficiency_type in ["deuteranopia", "protanopia"]:
# Shift toward blue
blue_shift = self._rgb_to_hex((
max(0, rgb[0] - 50),
max(0, rgb[1] - 30),
min(255, rgb[2] + 80)
))
suggestions.append({
"color": blue_shift,
"description": "Blue-shifted alternative",
"preserves": "approximate brightness"
})
# Shift toward yellow/orange
yellow_shift = self._rgb_to_hex((
min(255, rgb[0] + 50),
min(255, rgb[1] + 30),
max(0, rgb[2] - 80)
))
suggestions.append({
"color": yellow_shift,
"description": "Yellow-shifted alternative",
"preserves": "approximate brightness"
})
elif deficiency_type == "tritanopia":
# For blue-yellow deficiency, shift toward red or green
red_shift = self._rgb_to_hex((
min(255, rgb[0] + 60),
max(0, rgb[1] - 20),
max(0, rgb[2] - 40)
))
suggestions.append({
"color": red_shift,
"description": "Red-shifted alternative",
"preserves": "approximate brightness"
})
# Add safe palette suggestions
for palette_name, palette in SAFE_PALETTES.items():
# Find closest color in safe palette
min_distance = float('inf')
closest = None
for safe_color in palette["colors"]:
distance = self._get_color_distance(color, safe_color)
if distance < min_distance:
min_distance = distance
closest = safe_color
if closest:
suggestions.append({
"color": closest,
"description": f"From {palette['name']} palette",
"palette": palette_name
})
return {
"original_color": color,
"deficiency_type": deficiency_type,
"suggestions": suggestions[:5] # Limit to 5 suggestions
}
def _generate_recommendations(self, issues: List[Dict[str, Any]]) -> List[str]:
"""Generate actionable recommendations based on issues."""
recommendations = []
# Check for distinguishability issues
distinguishability_issues = [i for i in issues if i["type"] == "distinguishability"]
if distinguishability_issues:
affected_types = set()
for issue in distinguishability_issues:
affected_types.update(issue.get("affected_by", []))
if "deuteranopia" in affected_types or "protanopia" in affected_types:
recommendations.append(
"Avoid using red and green as the only differentiators - "
"add patterns, shapes, or labels"
)
recommendations.append(
"Consider using a color-blind safe palette like Okabe-Ito or IBM Design"
)
# Check for contrast issues
contrast_issues = [i for i in issues if i["type"] in ["contrast_ratio", "primary_contrast"]]
if contrast_issues:
recommendations.append(
"Increase contrast by darkening colors for light backgrounds "
"or lightening for dark backgrounds"
)
recommendations.append(
"Use WCAG contrast checker tools to verify text readability"
)
# General recommendations
if len(issues) > 0:
recommendations.append(
"Add secondary visual cues (icons, patterns, labels) "
"to not rely solely on color"
)
if not recommendations:
recommendations.append(
"Color palette appears accessible! Consider adding patterns "
"for additional distinguishability"
)
return recommendations

View File

@@ -1,533 +0,0 @@
"""
Chart creation tools using Plotly.
Provides tools for creating data visualizations with automatic theme integration.
"""
import base64
import logging
import os
from typing import Dict, List, Optional, Any, Union
logger = logging.getLogger(__name__)
# Check for kaleido availability
KALEIDO_AVAILABLE = False
try:
import kaleido
KALEIDO_AVAILABLE = True
except ImportError:
logger.debug("kaleido not installed - chart export will be unavailable")
# Default color palette based on Mantine theme
DEFAULT_COLORS = [
"#228be6", # blue
"#40c057", # green
"#fa5252", # red
"#fab005", # yellow
"#7950f2", # violet
"#fd7e14", # orange
"#20c997", # teal
"#f783ac", # pink
"#868e96", # gray
"#15aabf", # cyan
]
class ChartTools:
"""
Plotly-based chart creation tools.
Creates charts that integrate with DMC theming system.
"""
def __init__(self, theme_store=None):
"""
Initialize chart tools.
Args:
theme_store: Optional ThemeStore for theme token resolution
"""
self.theme_store = theme_store
self._active_theme = None
def set_theme(self, theme: Dict[str, Any]) -> None:
"""Set the active theme for chart styling."""
self._active_theme = theme
def _get_color_palette(self) -> List[str]:
"""Get color palette from theme or defaults."""
if self._active_theme and 'colors' in self._active_theme:
colors = self._active_theme['colors']
# Extract primary colors from theme
palette = []
for key in ['primary', 'secondary', 'success', 'warning', 'error']:
if key in colors:
palette.append(colors[key])
if palette:
return palette + DEFAULT_COLORS[len(palette):]
return DEFAULT_COLORS
def _resolve_color(self, color: Optional[str]) -> str:
"""Resolve a color token to actual color value."""
if not color:
return self._get_color_palette()[0]
# Check if it's a theme token
if self._active_theme and 'colors' in self._active_theme:
colors = self._active_theme['colors']
if color in colors:
return colors[color]
# Check if it's already a valid color
if color.startswith('#') or color.startswith('rgb'):
return color
# Map common color names to palette
color_map = {
'blue': DEFAULT_COLORS[0],
'green': DEFAULT_COLORS[1],
'red': DEFAULT_COLORS[2],
'yellow': DEFAULT_COLORS[3],
'violet': DEFAULT_COLORS[4],
'orange': DEFAULT_COLORS[5],
'teal': DEFAULT_COLORS[6],
'pink': DEFAULT_COLORS[7],
'gray': DEFAULT_COLORS[8],
'cyan': DEFAULT_COLORS[9],
}
return color_map.get(color, color)
async def chart_create(
self,
chart_type: str,
data: Dict[str, Any],
options: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Create a Plotly chart.
Args:
chart_type: Type of chart (line, bar, scatter, pie, heatmap, histogram, area)
data: Data specification with x, y values or labels/values for pie
options: Optional chart options (title, color, layout settings)
Returns:
Dict with:
- figure: Plotly figure JSON
- chart_type: Type of chart created
- error: Error message if creation failed
"""
options = options or {}
# Validate chart type
valid_types = ['line', 'bar', 'scatter', 'pie', 'heatmap', 'histogram', 'area']
if chart_type not in valid_types:
return {
"error": f"Invalid chart_type '{chart_type}'. Must be one of: {valid_types}",
"chart_type": chart_type,
"figure": None
}
try:
# Build trace based on chart type
trace = self._build_trace(chart_type, data, options)
if 'error' in trace:
return trace
# Build layout
layout = self._build_layout(options)
# Create figure structure
figure = {
"data": [trace],
"layout": layout
}
return {
"figure": figure,
"chart_type": chart_type,
"trace_count": 1
}
except Exception as e:
logger.error(f"Chart creation failed: {e}")
return {
"error": str(e),
"chart_type": chart_type,
"figure": None
}
def _build_trace(
self,
chart_type: str,
data: Dict[str, Any],
options: Dict[str, Any]
) -> Dict[str, Any]:
"""Build Plotly trace for the chart type."""
color = self._resolve_color(options.get('color'))
palette = self._get_color_palette()
# Common trace properties
trace: Dict[str, Any] = {}
if chart_type == 'line':
trace = {
"type": "scatter",
"mode": "lines+markers",
"x": data.get('x', []),
"y": data.get('y', []),
"line": {"color": color},
"marker": {"color": color}
}
if 'name' in data:
trace['name'] = data['name']
elif chart_type == 'bar':
trace = {
"type": "bar",
"x": data.get('x', []),
"y": data.get('y', []),
"marker": {"color": color}
}
if options.get('horizontal'):
trace['orientation'] = 'h'
trace['x'], trace['y'] = trace['y'], trace['x']
if 'name' in data:
trace['name'] = data['name']
elif chart_type == 'scatter':
trace = {
"type": "scatter",
"mode": "markers",
"x": data.get('x', []),
"y": data.get('y', []),
"marker": {
"color": color,
"size": options.get('marker_size', 10)
}
}
if 'size' in data:
trace['marker']['size'] = data['size']
if 'name' in data:
trace['name'] = data['name']
elif chart_type == 'pie':
labels = data.get('labels', data.get('x', []))
values = data.get('values', data.get('y', []))
trace = {
"type": "pie",
"labels": labels,
"values": values,
"marker": {"colors": palette[:len(labels)]}
}
if options.get('donut'):
trace['hole'] = options.get('hole', 0.4)
elif chart_type == 'heatmap':
trace = {
"type": "heatmap",
"z": data.get('z', data.get('values', [])),
"x": data.get('x', []),
"y": data.get('y', []),
"colorscale": options.get('colorscale', 'Blues')
}
elif chart_type == 'histogram':
trace = {
"type": "histogram",
"x": data.get('x', data.get('values', [])),
"marker": {"color": color}
}
if 'nbins' in options:
trace['nbinsx'] = options['nbins']
elif chart_type == 'area':
trace = {
"type": "scatter",
"mode": "lines",
"x": data.get('x', []),
"y": data.get('y', []),
"fill": "tozeroy",
"line": {"color": color},
"fillcolor": color.replace(')', ', 0.3)').replace('rgb', 'rgba') if color.startswith('rgb') else color + '4D'
}
if 'name' in data:
trace['name'] = data['name']
else:
return {"error": f"Unsupported chart type: {chart_type}"}
return trace
def _build_layout(self, options: Dict[str, Any]) -> Dict[str, Any]:
"""Build Plotly layout from options."""
layout: Dict[str, Any] = {
"autosize": True,
"margin": {"l": 50, "r": 30, "t": 50, "b": 50}
}
# Title
if 'title' in options:
layout['title'] = {
"text": options['title'],
"x": 0.5,
"xanchor": "center"
}
# Axis labels
if 'x_label' in options:
layout['xaxis'] = layout.get('xaxis', {})
layout['xaxis']['title'] = options['x_label']
if 'y_label' in options:
layout['yaxis'] = layout.get('yaxis', {})
layout['yaxis']['title'] = options['y_label']
# Theme-based styling
if self._active_theme:
colors = self._active_theme.get('colors', {})
bg = colors.get('background', {})
if isinstance(bg, dict):
layout['paper_bgcolor'] = bg.get('base', '#ffffff')
layout['plot_bgcolor'] = bg.get('subtle', '#f8f9fa')
elif isinstance(bg, str):
layout['paper_bgcolor'] = bg
layout['plot_bgcolor'] = bg
text_color = colors.get('text', {})
if isinstance(text_color, dict):
layout['font'] = {'color': text_color.get('primary', '#212529')}
elif isinstance(text_color, str):
layout['font'] = {'color': text_color}
# Additional layout options
if 'showlegend' in options:
layout['showlegend'] = options['showlegend']
if 'height' in options:
layout['height'] = options['height']
if 'width' in options:
layout['width'] = options['width']
return layout
async def chart_configure_interaction(
self,
figure: Dict[str, Any],
interactions: Dict[str, Any]
) -> Dict[str, Any]:
"""
Configure interactions for a chart.
Args:
figure: Plotly figure JSON to modify
interactions: Interaction configuration:
- hover_template: Custom hover text template
- click_data: Enable click data capture
- selection: Enable selection (box, lasso)
- zoom: Enable/disable zoom
Returns:
Dict with:
- figure: Updated figure JSON
- interactions_added: List of interactions configured
- error: Error message if configuration failed
"""
if not figure or 'data' not in figure:
return {
"error": "Invalid figure: must contain 'data' key",
"figure": figure,
"interactions_added": []
}
try:
interactions_added = []
# Process each trace
for i, trace in enumerate(figure['data']):
# Hover template
if 'hover_template' in interactions:
trace['hovertemplate'] = interactions['hover_template']
if i == 0:
interactions_added.append('hover_template')
# Custom hover info
if 'hover_info' in interactions:
trace['hoverinfo'] = interactions['hover_info']
if i == 0:
interactions_added.append('hover_info')
# Layout-level interactions
layout = figure.get('layout', {})
# Click data (Dash callback integration)
if interactions.get('click_data', False):
layout['clickmode'] = 'event+select'
interactions_added.append('click_data')
# Selection mode
if 'selection' in interactions:
sel_mode = interactions['selection']
if sel_mode in ['box', 'lasso', 'box+lasso']:
layout['dragmode'] = 'select' if sel_mode == 'box' else sel_mode
interactions_added.append(f'selection:{sel_mode}')
# Zoom configuration
if 'zoom' in interactions:
if not interactions['zoom']:
layout['xaxis'] = layout.get('xaxis', {})
layout['yaxis'] = layout.get('yaxis', {})
layout['xaxis']['fixedrange'] = True
layout['yaxis']['fixedrange'] = True
interactions_added.append('zoom:disabled')
else:
interactions_added.append('zoom:enabled')
# Modebar configuration
if 'modebar' in interactions:
layout['modebar'] = interactions['modebar']
interactions_added.append('modebar')
figure['layout'] = layout
return {
"figure": figure,
"interactions_added": interactions_added
}
except Exception as e:
logger.error(f"Interaction configuration failed: {e}")
return {
"error": str(e),
"figure": figure,
"interactions_added": []
}
async def chart_export(
self,
figure: Dict[str, Any],
format: str = "png",
width: Optional[int] = None,
height: Optional[int] = None,
scale: float = 2.0,
output_path: Optional[str] = None
) -> Dict[str, Any]:
"""
Export a Plotly chart to a static image format.
Args:
figure: Plotly figure JSON to export
format: Output format - png, svg, or pdf
width: Image width in pixels (default: from figure or 1200)
height: Image height in pixels (default: from figure or 800)
scale: Resolution scale factor (default: 2 for retina)
output_path: Optional file path to save the image
Returns:
Dict with:
- image_data: Base64-encoded image (if no output_path)
- file_path: Path to saved file (if output_path provided)
- format: Export format used
- dimensions: {width, height, scale}
- error: Error message if export failed
"""
# Validate format
valid_formats = ['png', 'svg', 'pdf']
format = format.lower()
if format not in valid_formats:
return {
"error": f"Invalid format '{format}'. Must be one of: {valid_formats}",
"format": format,
"image_data": None
}
# Check kaleido availability
if not KALEIDO_AVAILABLE:
return {
"error": "kaleido package not installed. Install with: pip install kaleido",
"format": format,
"image_data": None,
"install_hint": "pip install kaleido"
}
# Validate figure
if not figure or 'data' not in figure:
return {
"error": "Invalid figure: must contain 'data' key",
"format": format,
"image_data": None
}
try:
import plotly.graph_objects as go
import plotly.io as pio
# Create Plotly figure object
fig = go.Figure(figure)
# Determine dimensions
layout = figure.get('layout', {})
export_width = width or layout.get('width') or 1200
export_height = height or layout.get('height') or 800
# Export to bytes
image_bytes = pio.to_image(
fig,
format=format,
width=export_width,
height=export_height,
scale=scale
)
result = {
"format": format,
"dimensions": {
"width": export_width,
"height": export_height,
"scale": scale,
"effective_width": int(export_width * scale),
"effective_height": int(export_height * scale)
}
}
# Save to file or return base64
if output_path:
# Ensure directory exists
output_dir = os.path.dirname(output_path)
if output_dir and not os.path.exists(output_dir):
os.makedirs(output_dir, exist_ok=True)
# Add extension if missing
if not output_path.endswith(f'.{format}'):
output_path = f"{output_path}.{format}"
with open(output_path, 'wb') as f:
f.write(image_bytes)
result["file_path"] = output_path
result["file_size_bytes"] = len(image_bytes)
else:
# Return as base64
result["image_data"] = base64.b64encode(image_bytes).decode('utf-8')
result["data_uri"] = f"data:image/{format};base64,{result['image_data']}"
return result
except ImportError as e:
logger.error(f"Chart export failed - missing dependency: {e}")
return {
"error": f"Missing dependency for export: {e}",
"format": format,
"image_data": None,
"install_hint": "pip install plotly kaleido"
}
except Exception as e:
logger.error(f"Chart export failed: {e}")
return {
"error": str(e),
"format": format,
"image_data": None
}

View File

@@ -1,301 +0,0 @@
"""
DMC Component Registry for viz-platform.
Provides version-locked component definitions to prevent Claude from
hallucinating invalid props. Uses static JSON registries pre-generated
from DMC source.
"""
import json
import logging
from pathlib import Path
from typing import Dict, List, Optional, Any
logger = logging.getLogger(__name__)
class ComponentRegistry:
"""
Version-locked registry of Dash Mantine Components.
Loads component definitions from static JSON files and provides
lookup methods for validation tools.
"""
def __init__(self, dmc_version: Optional[str] = None):
"""
Initialize the component registry.
Args:
dmc_version: Installed DMC version (e.g., "0.14.7").
If None, will try to detect or use fallback.
"""
self.dmc_version = dmc_version
self.registry_dir = Path(__file__).parent.parent / 'registry'
self.components: Dict[str, Dict[str, Any]] = {}
self.categories: Dict[str, List[str]] = {}
self.loaded_version: Optional[str] = None
def load(self) -> bool:
"""
Load the component registry for the configured DMC version.
Returns:
True if registry loaded successfully, False otherwise
"""
registry_file = self._find_registry_file()
if not registry_file:
logger.warning(
f"No registry found for DMC {self.dmc_version}. "
"Component validation will be limited."
)
return False
try:
with open(registry_file, 'r') as f:
data = json.load(f)
self.loaded_version = data.get('version')
self.components = data.get('components', {})
self.categories = data.get('categories', {})
logger.info(
f"Loaded component registry v{self.loaded_version} "
f"with {len(self.components)} components"
)
return True
except Exception as e:
logger.error(f"Failed to load registry: {e}")
return False
def _find_registry_file(self) -> Optional[Path]:
"""
Find the best matching registry file for the DMC version.
Strategy:
1. Exact major.minor match (e.g., dmc_0_14.json for 0.14.7)
2. Fallback to latest available registry
Returns:
Path to registry file, or None if not found
"""
if not self.registry_dir.exists():
logger.warning(f"Registry directory not found: {self.registry_dir}")
return None
# Try exact major.minor match
if self.dmc_version:
parts = self.dmc_version.split('.')
if len(parts) >= 2:
major_minor = f"{parts[0]}_{parts[1]}"
exact_match = self.registry_dir / f"dmc_{major_minor}.json"
if exact_match.exists():
return exact_match
# Fallback: find latest registry
registry_files = list(self.registry_dir.glob("dmc_*.json"))
if registry_files:
# Sort by version and return latest
registry_files.sort(reverse=True)
fallback = registry_files[0]
if self.dmc_version:
logger.warning(
f"No exact match for DMC {self.dmc_version}, "
f"using fallback: {fallback.name}"
)
return fallback
return None
def get_component(self, name: str) -> Optional[Dict[str, Any]]:
"""
Get component definition by name.
Args:
name: Component name (e.g., "Button", "TextInput")
Returns:
Component definition dict, or None if not found
"""
return self.components.get(name)
def get_component_props(self, name: str) -> Optional[Dict[str, Any]]:
"""
Get props schema for a component.
Args:
name: Component name
Returns:
Props dict with type info, or None if component not found
"""
component = self.get_component(name)
if component:
return component.get('props', {})
return None
def list_components(self, category: Optional[str] = None) -> Dict[str, List[str]]:
"""
List available components, optionally filtered by category.
Args:
category: Optional category filter (e.g., "inputs", "buttons")
Returns:
Dict of category -> component names
"""
if category:
if category in self.categories:
return {category: self.categories[category]}
return {}
return self.categories
def get_categories(self) -> List[str]:
"""
Get list of available component categories.
Returns:
List of category names
"""
return list(self.categories.keys())
def validate_prop(
self,
component: str,
prop_name: str,
prop_value: Any
) -> Dict[str, Any]:
"""
Validate a single prop value against the registry.
Args:
component: Component name
prop_name: Prop name
prop_value: Value to validate
Returns:
Dict with valid: bool, error: Optional[str]
"""
props = self.get_component_props(component)
if props is None:
return {
'valid': False,
'error': f"Unknown component: {component}"
}
if prop_name not in props:
# Check for similar prop names (typo detection)
similar = self._find_similar_props(prop_name, props.keys())
if similar:
return {
'valid': False,
'error': f"Unknown prop '{prop_name}' for {component}. Did you mean '{similar}'?"
}
return {
'valid': False,
'error': f"Unknown prop '{prop_name}' for {component}"
}
prop_schema = props[prop_name]
return self._validate_value(prop_value, prop_schema, prop_name)
def _validate_value(
self,
value: Any,
schema: Dict[str, Any],
prop_name: str
) -> Dict[str, Any]:
"""
Validate a value against a prop schema.
Args:
value: Value to validate
schema: Prop schema from registry
prop_name: Prop name (for error messages)
Returns:
Dict with valid: bool, error: Optional[str]
"""
prop_type = schema.get('type', 'any')
# Any type always valid
if prop_type == 'any':
return {'valid': True}
# Check enum values
if 'enum' in schema:
if value not in schema['enum']:
return {
'valid': False,
'error': f"Prop '{prop_name}' expects one of {schema['enum']}, got '{value}'"
}
return {'valid': True}
# Type checking
type_checks = {
'string': lambda v: isinstance(v, str),
'number': lambda v: isinstance(v, (int, float)),
'integer': lambda v: isinstance(v, int),
'boolean': lambda v: isinstance(v, bool),
'array': lambda v: isinstance(v, list),
'object': lambda v: isinstance(v, dict),
}
checker = type_checks.get(prop_type)
if checker and not checker(value):
return {
'valid': False,
'error': f"Prop '{prop_name}' expects type '{prop_type}', got '{type(value).__name__}'"
}
return {'valid': True}
def _find_similar_props(
self,
prop_name: str,
available_props: List[str]
) -> Optional[str]:
"""
Find a similar prop name for typo suggestions.
Uses simple edit distance heuristic.
Args:
prop_name: The (possibly misspelled) prop name
available_props: List of valid prop names
Returns:
Most similar prop name, or None if no close match
"""
prop_lower = prop_name.lower()
for prop in available_props:
# Exact match after lowercase
if prop.lower() == prop_lower:
return prop
# Common typos: extra/missing letter
if abs(len(prop) - len(prop_name)) == 1:
if prop_lower.startswith(prop.lower()[:3]):
return prop
return None
def is_loaded(self) -> bool:
"""Check if registry is loaded."""
return len(self.components) > 0
def load_registry(dmc_version: Optional[str] = None) -> ComponentRegistry:
"""
Convenience function to load and return a component registry.
Args:
dmc_version: Optional DMC version string
Returns:
Loaded ComponentRegistry instance
"""
registry = ComponentRegistry(dmc_version)
registry.load()
return registry

View File

@@ -1,172 +0,0 @@
"""
Configuration loader for viz-platform MCP Server.
Implements hybrid configuration system:
- System-level: ~/.config/claude/viz-platform.env (theme preferences)
- Project-level: .env (DMC version overrides)
- Auto-detection: DMC package version from installed package
"""
from pathlib import Path
from dotenv import load_dotenv
import os
import logging
from typing import Dict, Optional
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class VizPlatformConfig:
"""Hybrid configuration loader for viz-platform tools"""
def __init__(self):
self.dmc_version: Optional[str] = None
self.theme_dir_user: Path = Path.home() / '.config' / 'claude' / 'themes'
self.theme_dir_project: Optional[Path] = None
self.default_theme: Optional[str] = None
def load(self) -> Dict[str, any]:
"""
Load configuration from system and project levels.
Returns:
Dict containing dmc_version, theme directories, and availability flags
"""
# Load system config
system_config = Path.home() / '.config' / 'claude' / 'viz-platform.env'
if system_config.exists():
load_dotenv(system_config)
logger.info(f"Loaded system configuration from {system_config}")
# Find project directory
project_dir = self._find_project_directory()
# Load project config (overrides system)
if project_dir:
project_config = project_dir / '.env'
if project_config.exists():
load_dotenv(project_config, override=True)
logger.info(f"Loaded project configuration from {project_config}")
# Set project theme directory
self.theme_dir_project = project_dir / '.viz-platform' / 'themes'
# Get DMC version (from env or auto-detect)
self.dmc_version = os.getenv('DMC_VERSION') or self._detect_dmc_version()
self.default_theme = os.getenv('VIZ_DEFAULT_THEME')
# Ensure user theme directory exists
self.theme_dir_user.mkdir(parents=True, exist_ok=True)
return {
'dmc_version': self.dmc_version,
'dmc_available': self.dmc_version is not None,
'theme_dir_user': str(self.theme_dir_user),
'theme_dir_project': str(self.theme_dir_project) if self.theme_dir_project else None,
'default_theme': self.default_theme,
'project_dir': str(project_dir) if project_dir else None
}
def _detect_dmc_version(self) -> Optional[str]:
"""
Auto-detect installed Dash Mantine Components version.
Returns:
Version string (e.g., "0.14.7") or None if not installed
"""
try:
from importlib.metadata import version
dmc_version = version('dash-mantine-components')
logger.info(f"Detected DMC version: {dmc_version}")
return dmc_version
except ImportError:
logger.warning("dash-mantine-components not installed - using registry fallback")
return None
except Exception as e:
logger.warning(f"Could not detect DMC version: {e}")
return None
def _find_project_directory(self) -> Optional[Path]:
"""
Find the user's project directory.
Returns:
Path to project directory, or None if not found
"""
# Strategy 1: Check CLAUDE_PROJECT_DIR environment variable
project_dir = os.getenv('CLAUDE_PROJECT_DIR')
if project_dir:
path = Path(project_dir)
if path.exists():
logger.info(f"Found project directory from CLAUDE_PROJECT_DIR: {path}")
return path
# Strategy 2: Check PWD
pwd = os.getenv('PWD')
if pwd:
path = Path(pwd)
if path.exists() and (
(path / '.git').exists() or
(path / '.env').exists() or
(path / '.viz-platform').exists()
):
logger.info(f"Found project directory from PWD: {path}")
return path
# Strategy 3: Check current working directory
cwd = Path.cwd()
if (cwd / '.git').exists() or (cwd / '.env').exists() or (cwd / '.viz-platform').exists():
logger.info(f"Found project directory from cwd: {cwd}")
return cwd
logger.debug("Could not determine project directory")
return None
def load_config() -> Dict[str, any]:
"""
Convenience function to load configuration.
Returns:
Configuration dictionary
"""
config = VizPlatformConfig()
return config.load()
def check_dmc_version() -> Dict[str, any]:
"""
Check DMC installation status for SessionStart hook.
Returns:
Dict with installation status and version info
"""
config = load_config()
if not config.get('dmc_available'):
return {
'installed': False,
'message': 'dash-mantine-components not installed. Run: pip install dash-mantine-components'
}
version = config.get('dmc_version', 'unknown')
# Check for registry compatibility
registry_path = Path(__file__).parent.parent / 'registry'
major_minor = '.'.join(version.split('.')[:2]) if version else None
registry_file = registry_path / f'dmc_{major_minor.replace(".", "_")}.json' if major_minor else None
if registry_file and registry_file.exists():
return {
'installed': True,
'version': version,
'registry_available': True,
'message': f'DMC {version} ready with component registry'
}
else:
return {
'installed': True,
'version': version,
'registry_available': False,
'message': f'DMC {version} installed but no matching registry. Validation may be limited.'
}

View File

@@ -1,306 +0,0 @@
"""
DMC (Dash Mantine Components) validation tools.
Provides component constraint layer to prevent Claude from hallucinating invalid props.
"""
import logging
from typing import Dict, List, Optional, Any
from .component_registry import ComponentRegistry
logger = logging.getLogger(__name__)
class DMCTools:
"""
DMC component validation tools.
These tools provide the "constraint layer" that validates component usage
against a version-locked registry of DMC components.
"""
def __init__(self, registry: Optional[ComponentRegistry] = None):
"""
Initialize DMC tools with component registry.
Args:
registry: ComponentRegistry instance. If None, creates one.
"""
self.registry = registry
self._initialized = False
def initialize(self, dmc_version: Optional[str] = None) -> bool:
"""
Initialize the registry if not already provided.
Args:
dmc_version: DMC version to load registry for
Returns:
True if initialized successfully
"""
if self.registry is None:
self.registry = ComponentRegistry(dmc_version)
if not self.registry.is_loaded():
self.registry.load()
self._initialized = self.registry.is_loaded()
return self._initialized
async def list_components(
self,
category: Optional[str] = None
) -> Dict[str, Any]:
"""
List available DMC components, optionally filtered by category.
Args:
category: Optional category filter (e.g., "inputs", "buttons", "navigation")
Returns:
Dict with:
- components: Dict[category -> [component names]]
- categories: List of available categories
- version: Loaded DMC registry version
- total_count: Total number of components
"""
if not self._initialized:
return {
"error": "Registry not initialized",
"components": {},
"categories": [],
"version": None,
"total_count": 0
}
components = self.registry.list_components(category)
all_categories = self.registry.get_categories()
# Count total components
total = sum(len(comps) for comps in components.values())
return {
"components": components,
"categories": all_categories if not category else [category],
"version": self.registry.loaded_version,
"total_count": total
}
async def get_component_props(self, component: str) -> Dict[str, Any]:
"""
Get props schema for a specific component.
Args:
component: Component name (e.g., "Button", "TextInput")
Returns:
Dict with:
- component: Component name
- description: Component description
- props: Dict of prop name -> {type, default, enum, description}
- prop_count: Number of props
- required: List of required prop names
Or error dict if component not found
"""
if not self._initialized:
return {
"error": "Registry not initialized",
"component": component,
"props": {},
"prop_count": 0
}
comp_def = self.registry.get_component(component)
if not comp_def:
# Try to suggest similar component name
similar = self._find_similar_component(component)
error_msg = f"Component '{component}' not found in registry"
if similar:
error_msg += f". Did you mean '{similar}'?"
return {
"error": error_msg,
"component": component,
"props": {},
"prop_count": 0
}
props = comp_def.get('props', {})
# Extract required props
required = [
name for name, schema in props.items()
if schema.get('required', False)
]
return {
"component": component,
"description": comp_def.get('description', ''),
"props": props,
"prop_count": len(props),
"required": required,
"version": self.registry.loaded_version
}
async def validate_component(
self,
component: str,
props: Dict[str, Any]
) -> Dict[str, Any]:
"""
Validate component props against registry.
Args:
component: Component name
props: Props dict to validate
Returns:
Dict with:
- valid: bool - True if all props are valid
- errors: List of error messages
- warnings: List of warning messages
- validated_props: Number of props validated
- component: Component name for reference
"""
if not self._initialized:
return {
"valid": False,
"errors": ["Registry not initialized"],
"warnings": [],
"validated_props": 0,
"component": component
}
errors: List[str] = []
warnings: List[str] = []
# Check if component exists
comp_def = self.registry.get_component(component)
if not comp_def:
similar = self._find_similar_component(component)
error_msg = f"Unknown component: {component}"
if similar:
error_msg += f". Did you mean '{similar}'?"
errors.append(error_msg)
return {
"valid": False,
"errors": errors,
"warnings": warnings,
"validated_props": 0,
"component": component
}
comp_props = comp_def.get('props', {})
# Check for required props
for prop_name, prop_schema in comp_props.items():
if prop_schema.get('required', False) and prop_name not in props:
errors.append(f"Missing required prop: '{prop_name}'")
# Validate each provided prop
for prop_name, prop_value in props.items():
# Skip special props that are always allowed
if prop_name in ('id', 'children', 'className', 'style', 'key'):
continue
result = self.registry.validate_prop(component, prop_name, prop_value)
if not result.get('valid', True):
error = result.get('error', f"Invalid prop: {prop_name}")
# Distinguish between typos/unknown props and type errors
if "Unknown prop" in error:
errors.append(f"{error}")
elif "expects one of" in error:
errors.append(f"{error}")
elif "expects type" in error:
warnings.append(f"⚠️ {error}")
else:
errors.append(f"{error}")
# Check for props that exist but might have common mistakes
self._check_common_mistakes(component, props, warnings)
return {
"valid": len(errors) == 0,
"errors": errors,
"warnings": warnings,
"validated_props": len(props),
"component": component,
"version": self.registry.loaded_version
}
def _find_similar_component(self, component: str) -> Optional[str]:
"""
Find a similar component name for suggestions.
Args:
component: The (possibly misspelled) component name
Returns:
Similar component name, or None if no close match
"""
if not self.registry:
return None
comp_lower = component.lower()
all_components = []
for comps in self.registry.categories.values():
all_components.extend(comps)
for comp in all_components:
# Exact match after lowercase
if comp.lower() == comp_lower:
return comp
# Check if it's a prefix match
if comp.lower().startswith(comp_lower) or comp_lower.startswith(comp.lower()):
return comp
# Check for common typos
if abs(len(comp) - len(component)) <= 2:
if comp_lower[:4] == comp.lower()[:4]:
return comp
return None
def _check_common_mistakes(
self,
component: str,
props: Dict[str, Any],
warnings: List[str]
) -> None:
"""
Check for common prop usage mistakes and add warnings.
Args:
component: Component name
props: Props being used
warnings: List to append warnings to
"""
# Common mistake: using 'onclick' instead of callback pattern
if 'onclick' in [p.lower() for p in props.keys()]:
warnings.append(
"⚠️ Dash uses callback patterns, not inline event handlers. "
"Use 'n_clicks' prop with a callback instead."
)
# Common mistake: using 'class' instead of 'className'
if 'class' in props:
warnings.append(
"⚠️ Use 'className' instead of 'class' for CSS classes."
)
# Button-specific checks
if component == 'Button':
if 'href' in props and 'component' not in props:
warnings.append(
"⚠️ Button with 'href' should also set 'component=\"a\"' for proper anchor behavior."
)
# Input-specific checks
if 'Input' in component:
if 'value' in props and 'onChange' in [p for p in props.keys()]:
warnings.append(
"⚠️ Dash uses 'value' prop with callbacks, not 'onChange'. "
"The value updates automatically through Dash callbacks."
)

View File

@@ -1,553 +0,0 @@
"""
Layout composition tools for dashboard building.
Provides tools for creating structured layouts with grids, filters, and sections.
"""
import logging
from typing import Dict, List, Optional, Any
from uuid import uuid4
logger = logging.getLogger(__name__)
# Standard responsive breakpoints (Mantine/Bootstrap-aligned)
DEFAULT_BREAKPOINTS = {
"xs": {
"min_width": "0px",
"max_width": "575px",
"cols": 1,
"spacing": "xs",
"description": "Extra small devices (phones, portrait)"
},
"sm": {
"min_width": "576px",
"max_width": "767px",
"cols": 2,
"spacing": "sm",
"description": "Small devices (phones, landscape)"
},
"md": {
"min_width": "768px",
"max_width": "991px",
"cols": 6,
"spacing": "md",
"description": "Medium devices (tablets)"
},
"lg": {
"min_width": "992px",
"max_width": "1199px",
"cols": 12,
"spacing": "md",
"description": "Large devices (desktops)"
},
"xl": {
"min_width": "1200px",
"max_width": None,
"cols": 12,
"spacing": "lg",
"description": "Extra large devices (large desktops)"
}
}
# Layout templates
TEMPLATES = {
"dashboard": {
"sections": ["header", "filters", "main", "footer"],
"default_grid": {"cols": 12, "spacing": "md"},
"description": "Standard dashboard with header, filters, main content, and footer"
},
"report": {
"sections": ["title", "summary", "content", "appendix"],
"default_grid": {"cols": 1, "spacing": "lg"},
"description": "Report layout with title, summary, and content sections"
},
"form": {
"sections": ["header", "fields", "actions"],
"default_grid": {"cols": 2, "spacing": "md"},
"description": "Form layout with header, fields, and action buttons"
},
"blank": {
"sections": ["main"],
"default_grid": {"cols": 12, "spacing": "md"},
"description": "Blank canvas for custom layouts"
}
}
# Filter type definitions
FILTER_TYPES = {
"dropdown": {
"component": "Select",
"props": ["label", "data", "placeholder", "clearable", "searchable", "value"]
},
"multi_select": {
"component": "MultiSelect",
"props": ["label", "data", "placeholder", "clearable", "searchable", "value"]
},
"date_range": {
"component": "DateRangePicker",
"props": ["label", "placeholder", "value", "minDate", "maxDate"]
},
"date": {
"component": "DatePicker",
"props": ["label", "placeholder", "value", "minDate", "maxDate"]
},
"search": {
"component": "TextInput",
"props": ["label", "placeholder", "value", "icon"]
},
"checkbox_group": {
"component": "CheckboxGroup",
"props": ["label", "children", "value"]
},
"radio_group": {
"component": "RadioGroup",
"props": ["label", "children", "value"]
},
"slider": {
"component": "Slider",
"props": ["label", "min", "max", "step", "value", "marks"]
},
"range_slider": {
"component": "RangeSlider",
"props": ["label", "min", "max", "step", "value", "marks"]
}
}
class LayoutTools:
"""
Dashboard layout composition tools.
Creates layouts that map to DMC Grid and AppShell components.
"""
def __init__(self):
"""Initialize layout tools."""
self._layouts: Dict[str, Dict[str, Any]] = {}
async def layout_create(
self,
name: str,
template: Optional[str] = None
) -> Dict[str, Any]:
"""
Create a new layout container.
Args:
name: Unique name for the layout
template: Optional template (dashboard, report, form, blank)
Returns:
Dict with:
- layout_ref: Reference to use in other tools
- template: Template used
- sections: Available sections
- grid: Default grid configuration
"""
# Validate template
template = template or "blank"
if template not in TEMPLATES:
return {
"error": f"Invalid template '{template}'. Must be one of: {list(TEMPLATES.keys())}",
"layout_ref": None
}
# Check for name collision
if name in self._layouts:
return {
"error": f"Layout '{name}' already exists. Use a different name or modify existing.",
"layout_ref": name
}
template_config = TEMPLATES[template]
# Create layout structure
layout = {
"id": str(uuid4()),
"name": name,
"template": template,
"sections": {section: {"items": []} for section in template_config["sections"]},
"grid": template_config["default_grid"].copy(),
"filters": [],
"metadata": {
"description": template_config["description"]
}
}
self._layouts[name] = layout
return {
"layout_ref": name,
"template": template,
"sections": template_config["sections"],
"grid": layout["grid"],
"description": template_config["description"]
}
async def layout_add_filter(
self,
layout_ref: str,
filter_type: str,
options: Dict[str, Any]
) -> Dict[str, Any]:
"""
Add a filter control to a layout.
Args:
layout_ref: Layout name to add filter to
filter_type: Type of filter (dropdown, date_range, search, checkbox_group, etc.)
options: Filter options (label, data for dropdown, placeholder, position)
Returns:
Dict with:
- filter_id: Unique ID for the filter
- component: DMC component that will be used
- props: Props that were set
- position: Where filter was placed
"""
# Validate layout exists
if layout_ref not in self._layouts:
return {
"error": f"Layout '{layout_ref}' not found. Create it first with layout_create.",
"filter_id": None
}
# Validate filter type
if filter_type not in FILTER_TYPES:
return {
"error": f"Invalid filter_type '{filter_type}'. Must be one of: {list(FILTER_TYPES.keys())}",
"filter_id": None
}
filter_config = FILTER_TYPES[filter_type]
layout = self._layouts[layout_ref]
# Generate filter ID
filter_id = f"filter_{filter_type}_{len(layout['filters'])}"
# Extract relevant props
props = {"id": filter_id}
for prop in filter_config["props"]:
if prop in options:
props[prop] = options[prop]
# Determine position
position = options.get("position", "filters")
if position not in layout["sections"]:
# Default to first available section
position = list(layout["sections"].keys())[0]
# Create filter definition
filter_def = {
"id": filter_id,
"type": filter_type,
"component": filter_config["component"],
"props": props,
"position": position
}
layout["filters"].append(filter_def)
layout["sections"][position]["items"].append({
"type": "filter",
"ref": filter_id
})
return {
"filter_id": filter_id,
"component": filter_config["component"],
"props": props,
"position": position,
"layout_ref": layout_ref
}
async def layout_set_grid(
self,
layout_ref: str,
grid: Dict[str, Any]
) -> Dict[str, Any]:
"""
Configure the grid system for a layout.
Args:
layout_ref: Layout name to configure
grid: Grid configuration:
- cols: Number of columns (default 12)
- spacing: Gap between items (xs, sm, md, lg, xl)
- breakpoints: Responsive breakpoints {xs: cols, sm: cols, ...}
- gutter: Gutter size
Returns:
Dict with:
- grid: Updated grid configuration
- layout_ref: Layout reference
"""
# Validate layout exists
if layout_ref not in self._layouts:
return {
"error": f"Layout '{layout_ref}' not found. Create it first with layout_create.",
"grid": None
}
layout = self._layouts[layout_ref]
# Validate spacing if provided
valid_spacing = ["xs", "sm", "md", "lg", "xl"]
if "spacing" in grid and grid["spacing"] not in valid_spacing:
return {
"error": f"Invalid spacing '{grid['spacing']}'. Must be one of: {valid_spacing}",
"grid": layout["grid"]
}
# Validate cols
if "cols" in grid:
cols = grid["cols"]
if not isinstance(cols, int) or cols < 1 or cols > 24:
return {
"error": f"Invalid cols '{cols}'. Must be integer between 1 and 24.",
"grid": layout["grid"]
}
# Update grid configuration
layout["grid"].update(grid)
# Process breakpoints if provided
if "breakpoints" in grid:
bp = grid["breakpoints"]
layout["grid"]["breakpoints"] = bp
return {
"grid": layout["grid"],
"layout_ref": layout_ref
}
async def layout_get(self, layout_ref: str) -> Dict[str, Any]:
"""
Get a layout's full configuration.
Args:
layout_ref: Layout name to retrieve
Returns:
Full layout configuration or error
"""
if layout_ref not in self._layouts:
return {
"error": f"Layout '{layout_ref}' not found.",
"layout": None
}
layout = self._layouts[layout_ref]
return {
"layout": layout,
"filter_count": len(layout["filters"]),
"sections": list(layout["sections"].keys())
}
async def layout_add_section(
self,
layout_ref: str,
section_name: str,
position: Optional[int] = None
) -> Dict[str, Any]:
"""
Add a custom section to a layout.
Args:
layout_ref: Layout name
section_name: Name for the new section
position: Optional position index (appends if not specified)
Returns:
Dict with sections list and the new section name
"""
if layout_ref not in self._layouts:
return {
"error": f"Layout '{layout_ref}' not found.",
"sections": []
}
layout = self._layouts[layout_ref]
if section_name in layout["sections"]:
return {
"error": f"Section '{section_name}' already exists.",
"sections": list(layout["sections"].keys())
}
# Add new section
layout["sections"][section_name] = {"items": []}
return {
"section_name": section_name,
"sections": list(layout["sections"].keys()),
"layout_ref": layout_ref
}
def get_available_templates(self) -> Dict[str, Any]:
"""Get list of available layout templates."""
return {
name: {
"sections": config["sections"],
"description": config["description"]
}
for name, config in TEMPLATES.items()
}
def get_available_filter_types(self) -> Dict[str, Any]:
"""Get list of available filter types."""
return {
name: {
"component": config["component"],
"props": config["props"]
}
for name, config in FILTER_TYPES.items()
}
async def layout_set_breakpoints(
self,
layout_ref: str,
breakpoints: Dict[str, Dict[str, Any]],
mobile_first: bool = True
) -> Dict[str, Any]:
"""
Configure responsive breakpoints for a layout.
Args:
layout_ref: Layout name to configure
breakpoints: Breakpoint configuration dict:
{
"xs": {"cols": 1, "spacing": "xs"},
"sm": {"cols": 2, "spacing": "sm"},
"md": {"cols": 6, "spacing": "md"},
"lg": {"cols": 12, "spacing": "md"},
"xl": {"cols": 12, "spacing": "lg"}
}
mobile_first: If True, use min-width media queries (default)
Returns:
Dict with:
- breakpoints: Complete breakpoint configuration
- css_media_queries: Generated CSS media queries
- mobile_first: Whether mobile-first approach is used
"""
# Validate layout exists
if layout_ref not in self._layouts:
return {
"error": f"Layout '{layout_ref}' not found. Create it first with layout_create.",
"breakpoints": None
}
layout = self._layouts[layout_ref]
# Validate breakpoint names
valid_breakpoints = ["xs", "sm", "md", "lg", "xl"]
for bp_name in breakpoints.keys():
if bp_name not in valid_breakpoints:
return {
"error": f"Invalid breakpoint '{bp_name}'. Must be one of: {valid_breakpoints}",
"breakpoints": layout.get("breakpoints")
}
# Merge with defaults
merged_breakpoints = {}
for bp_name in valid_breakpoints:
default = DEFAULT_BREAKPOINTS[bp_name].copy()
if bp_name in breakpoints:
default.update(breakpoints[bp_name])
merged_breakpoints[bp_name] = default
# Validate spacing values
valid_spacing = ["xs", "sm", "md", "lg", "xl"]
for bp_name, bp_config in merged_breakpoints.items():
if "spacing" in bp_config and bp_config["spacing"] not in valid_spacing:
return {
"error": f"Invalid spacing '{bp_config['spacing']}' for breakpoint '{bp_name}'. Must be one of: {valid_spacing}",
"breakpoints": layout.get("breakpoints")
}
# Validate column counts
for bp_name, bp_config in merged_breakpoints.items():
if "cols" in bp_config:
cols = bp_config["cols"]
if not isinstance(cols, int) or cols < 1 or cols > 24:
return {
"error": f"Invalid cols '{cols}' for breakpoint '{bp_name}'. Must be integer between 1 and 24.",
"breakpoints": layout.get("breakpoints")
}
# Generate CSS media queries
css_queries = self._generate_media_queries(merged_breakpoints, mobile_first)
# Store in layout
layout["breakpoints"] = merged_breakpoints
layout["mobile_first"] = mobile_first
layout["responsive_css"] = css_queries
return {
"layout_ref": layout_ref,
"breakpoints": merged_breakpoints,
"mobile_first": mobile_first,
"css_media_queries": css_queries
}
def _generate_media_queries(
self,
breakpoints: Dict[str, Dict[str, Any]],
mobile_first: bool
) -> List[str]:
"""Generate CSS media queries for breakpoints."""
queries = []
bp_order = ["xs", "sm", "md", "lg", "xl"]
if mobile_first:
# Use min-width queries (mobile-first)
for bp_name in bp_order[1:]: # Skip xs (base styles)
bp = breakpoints[bp_name]
min_width = bp.get("min_width", DEFAULT_BREAKPOINTS[bp_name]["min_width"])
if min_width and min_width != "0px":
queries.append(f"@media (min-width: {min_width}) {{ /* {bp_name} styles */ }}")
else:
# Use max-width queries (desktop-first)
for bp_name in reversed(bp_order[:-1]): # Skip xl (base styles)
bp = breakpoints[bp_name]
max_width = bp.get("max_width", DEFAULT_BREAKPOINTS[bp_name]["max_width"])
if max_width:
queries.append(f"@media (max-width: {max_width}) {{ /* {bp_name} styles */ }}")
return queries
async def layout_get_breakpoints(self, layout_ref: str) -> Dict[str, Any]:
"""
Get the breakpoint configuration for a layout.
Args:
layout_ref: Layout name
Returns:
Dict with breakpoint configuration
"""
if layout_ref not in self._layouts:
return {
"error": f"Layout '{layout_ref}' not found.",
"breakpoints": None
}
layout = self._layouts[layout_ref]
return {
"layout_ref": layout_ref,
"breakpoints": layout.get("breakpoints", DEFAULT_BREAKPOINTS.copy()),
"mobile_first": layout.get("mobile_first", True),
"css_media_queries": layout.get("responsive_css", [])
}
def get_default_breakpoints(self) -> Dict[str, Any]:
"""Get the default breakpoint configuration."""
return {
"breakpoints": DEFAULT_BREAKPOINTS.copy(),
"description": "Standard responsive breakpoints aligned with Mantine/Bootstrap",
"mobile_first": True
}

View File

@@ -1,366 +0,0 @@
"""
Multi-page app tools for viz-platform.
Provides tools for building complete Dash applications with routing and navigation.
"""
import logging
from typing import Dict, List, Optional, Any
from uuid import uuid4
logger = logging.getLogger(__name__)
# Navigation position options
NAV_POSITIONS = ["top", "left", "right"]
# Auth types supported
AUTH_TYPES = ["none", "basic", "oauth", "custom"]
class PageTools:
"""
Multi-page Dash application tools.
Creates page definitions, navigation, and auth configuration.
"""
def __init__(self):
"""Initialize page tools."""
self._pages: Dict[str, Dict[str, Any]] = {}
self._navbars: Dict[str, Dict[str, Any]] = {}
self._app_config: Dict[str, Any] = {
"title": "Dash App",
"suppress_callback_exceptions": True
}
async def page_create(
self,
name: str,
path: str,
layout_ref: Optional[str] = None,
title: Optional[str] = None
) -> Dict[str, Any]:
"""
Create a new page definition.
Args:
name: Unique page name (used as identifier)
path: URL path for the page (e.g., "/", "/settings")
layout_ref: Optional layout reference to use for the page
title: Optional page title (defaults to name)
Returns:
Dict with:
- page_ref: Reference to use in other tools
- path: URL path
- registered: Whether page was registered
"""
# Validate path format
if not path.startswith('/'):
return {
"error": f"Path must start with '/'. Got: {path}",
"page_ref": None
}
# Check for name collision
if name in self._pages:
return {
"error": f"Page '{name}' already exists. Use a different name.",
"page_ref": name
}
# Check for path collision
for page_name, page_data in self._pages.items():
if page_data['path'] == path:
return {
"error": f"Path '{path}' already used by page '{page_name}'.",
"page_ref": None
}
# Create page definition
page = {
"id": str(uuid4()),
"name": name,
"path": path,
"title": title or name,
"layout_ref": layout_ref,
"auth": None,
"metadata": {}
}
self._pages[name] = page
return {
"page_ref": name,
"path": path,
"title": page["title"],
"layout_ref": layout_ref,
"registered": True
}
async def page_add_navbar(
self,
pages: List[str],
options: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Generate a navigation component linking pages.
Args:
pages: List of page names to include in navigation
options: Navigation options:
- position: "top", "left", or "right"
- style: Style variant
- brand: Brand/logo text or config
- collapsible: Whether to collapse on mobile
Returns:
Dict with:
- navbar_id: Navigation ID
- pages: List of page links generated
- component: DMC component structure
"""
options = options or {}
# Validate pages exist
missing_pages = [p for p in pages if p not in self._pages]
if missing_pages:
return {
"error": f"Pages not found: {missing_pages}. Create them first.",
"navbar_id": None
}
# Validate position
position = options.get("position", "top")
if position not in NAV_POSITIONS:
return {
"error": f"Invalid position '{position}'. Must be one of: {NAV_POSITIONS}",
"navbar_id": None
}
# Generate navbar ID
navbar_id = f"navbar_{len(self._navbars)}"
# Build page links
page_links = []
for page_name in pages:
page = self._pages[page_name]
page_links.append({
"label": page["title"],
"href": page["path"],
"page_ref": page_name
})
# Build DMC component structure
if position == "top":
component = self._build_top_navbar(page_links, options)
else:
component = self._build_side_navbar(page_links, options, position)
# Store navbar config
self._navbars[navbar_id] = {
"id": navbar_id,
"position": position,
"pages": pages,
"options": options,
"component": component
}
return {
"navbar_id": navbar_id,
"position": position,
"pages": page_links,
"component": component
}
async def page_set_auth(
self,
page_ref: str,
auth_config: Dict[str, Any]
) -> Dict[str, Any]:
"""
Configure authentication for a page.
Args:
page_ref: Page name to configure
auth_config: Authentication configuration:
- type: "none", "basic", "oauth", "custom"
- required: Whether auth is required (default True)
- roles: List of required roles (optional)
- redirect: Redirect path for unauthenticated users
Returns:
Dict with:
- page_ref: Page reference
- auth_type: Type of auth configured
- protected: Whether page is protected
"""
# Validate page exists
if page_ref not in self._pages:
available = list(self._pages.keys())
return {
"error": f"Page '{page_ref}' not found. Available: {available}",
"page_ref": page_ref
}
# Validate auth type
auth_type = auth_config.get("type", "basic")
if auth_type not in AUTH_TYPES:
return {
"error": f"Invalid auth type '{auth_type}'. Must be one of: {AUTH_TYPES}",
"page_ref": page_ref
}
# Build auth config
auth = {
"type": auth_type,
"required": auth_config.get("required", True),
"roles": auth_config.get("roles", []),
"redirect": auth_config.get("redirect", "/login")
}
# Handle OAuth-specific config
if auth_type == "oauth":
auth["provider"] = auth_config.get("provider", "generic")
auth["scopes"] = auth_config.get("scopes", [])
# Update page
self._pages[page_ref]["auth"] = auth
return {
"page_ref": page_ref,
"auth_type": auth_type,
"protected": auth["required"],
"roles": auth["roles"],
"redirect": auth["redirect"]
}
async def page_list(self) -> Dict[str, Any]:
"""
List all registered pages.
Returns:
Dict with pages and their configurations
"""
pages_info = {}
for name, page in self._pages.items():
pages_info[name] = {
"path": page["path"],
"title": page["title"],
"layout_ref": page["layout_ref"],
"protected": page["auth"] is not None and page["auth"].get("required", False)
}
return {
"pages": pages_info,
"count": len(pages_info),
"navbars": list(self._navbars.keys())
}
async def page_get_app_config(self) -> Dict[str, Any]:
"""
Get the complete app configuration for Dash.
Returns:
Dict with app config including pages, navbars, and settings
"""
# Build pages config
pages_config = []
for name, page in self._pages.items():
pages_config.append({
"name": name,
"path": page["path"],
"title": page["title"],
"layout_ref": page["layout_ref"]
})
# Build routing config
routes = {page["path"]: name for name, page in self._pages.items()}
return {
"app": self._app_config,
"pages": pages_config,
"routes": routes,
"navbars": list(self._navbars.values()),
"page_count": len(self._pages)
}
def _build_top_navbar(
self,
page_links: List[Dict[str, str]],
options: Dict[str, Any]
) -> Dict[str, Any]:
"""Build a top navigation bar component."""
brand = options.get("brand", "App")
# Build nav links
nav_items = []
for link in page_links:
nav_items.append({
"component": "NavLink",
"props": {
"label": link["label"],
"href": link["href"]
}
})
return {
"component": "AppShell.Header",
"children": [
{
"component": "Group",
"props": {"justify": "space-between", "h": "100%", "px": "md"},
"children": [
{
"component": "Text",
"props": {"size": "lg", "fw": 700},
"children": brand
},
{
"component": "Group",
"props": {"gap": "sm"},
"children": nav_items
}
]
}
]
}
def _build_side_navbar(
self,
page_links: List[Dict[str, str]],
options: Dict[str, Any],
position: str
) -> Dict[str, Any]:
"""Build a side navigation bar component."""
brand = options.get("brand", "App")
# Build nav links
nav_items = []
for link in page_links:
nav_items.append({
"component": "NavLink",
"props": {
"label": link["label"],
"href": link["href"]
}
})
navbar_component = "AppShell.Navbar" if position == "left" else "AppShell.Aside"
return {
"component": navbar_component,
"props": {"p": "md"},
"children": [
{
"component": "Text",
"props": {"size": "lg", "fw": 700, "mb": "md"},
"children": brand
},
{
"component": "Stack",
"props": {"gap": "xs"},
"children": nav_items
}
]
}

View File

@@ -1,928 +0,0 @@
"""
MCP Server entry point for viz-platform integration.
Provides Dash Mantine Components validation, charting, layout, theming, and page tools
to Claude Code via JSON-RPC 2.0 over stdio.
"""
import asyncio
import logging
import json
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
from .config import VizPlatformConfig
from .dmc_tools import DMCTools
from .chart_tools import ChartTools
from .layout_tools import LayoutTools
from .theme_tools import ThemeTools
from .page_tools import PageTools
from .accessibility_tools import AccessibilityTools
# Suppress noisy MCP validation warnings on stderr
logging.basicConfig(level=logging.INFO)
logging.getLogger("root").setLevel(logging.ERROR)
logging.getLogger("mcp").setLevel(logging.ERROR)
logger = logging.getLogger(__name__)
class VizPlatformMCPServer:
"""MCP Server for visualization platform integration"""
def __init__(self):
self.server = Server("viz-platform-mcp")
self.config = None
self.dmc_tools = DMCTools()
self.chart_tools = ChartTools()
self.layout_tools = LayoutTools()
self.theme_tools = ThemeTools()
self.page_tools = PageTools()
self.accessibility_tools = AccessibilityTools(theme_store=self.theme_tools.store)
async def initialize(self):
"""Initialize server and load configuration."""
try:
config_loader = VizPlatformConfig()
self.config = config_loader.load()
# Initialize DMC tools with detected version
dmc_version = self.config.get('dmc_version')
self.dmc_tools.initialize(dmc_version)
# Log available capabilities
caps = []
if self.config.get('dmc_available'):
caps.append(f"DMC {dmc_version}")
if self.dmc_tools._initialized:
caps.append(f"Registry loaded ({self.dmc_tools.registry.loaded_version})")
else:
caps.append("DMC (not installed)")
logger.info(f"viz-platform MCP Server initialized with: {', '.join(caps)}")
except Exception as e:
logger.error(f"Failed to initialize: {e}")
raise
def setup_tools(self):
"""Register all available tools with the MCP server"""
@self.server.list_tools()
async def list_tools() -> list[Tool]:
"""Return list of available tools"""
tools = []
# DMC validation tools (Issue #172)
tools.append(Tool(
name="list_components",
description=(
"List available Dash Mantine Components. "
"Returns components grouped by category with version info. "
"Use this to discover what components are available before building UI."
),
inputSchema={
"type": "object",
"properties": {
"category": {
"type": "string",
"description": (
"Optional category filter. Available categories: "
"buttons, inputs, navigation, feedback, overlays, "
"typography, layout, data_display, charts, dates"
)
}
},
"required": []
}
))
tools.append(Tool(
name="get_component_props",
description=(
"Get the props schema for a specific DMC component. "
"Returns all available props with types, defaults, and allowed values. "
"ALWAYS use this before creating a component to ensure valid props."
),
inputSchema={
"type": "object",
"properties": {
"component": {
"type": "string",
"description": "Component name (e.g., 'Button', 'TextInput', 'Select')"
}
},
"required": ["component"]
}
))
tools.append(Tool(
name="validate_component",
description=(
"Validate component props before use. "
"Checks for invalid props, type mismatches, and common mistakes. "
"Returns errors and warnings with suggestions for fixes."
),
inputSchema={
"type": "object",
"properties": {
"component": {
"type": "string",
"description": "Component name to validate"
},
"props": {
"type": "object",
"description": "Props object to validate"
}
},
"required": ["component", "props"]
}
))
# Chart tools (Issue #173)
tools.append(Tool(
name="chart_create",
description=(
"Create a Plotly chart for data visualization. "
"Supports line, bar, scatter, pie, heatmap, histogram, and area charts. "
"Automatically applies theme colors when a theme is active."
),
inputSchema={
"type": "object",
"properties": {
"chart_type": {
"type": "string",
"enum": ["line", "bar", "scatter", "pie", "heatmap", "histogram", "area"],
"description": "Type of chart to create"
},
"data": {
"type": "object",
"description": (
"Data for the chart. For most charts: {x: [], y: []}. "
"For pie: {labels: [], values: []}. "
"For heatmap: {x: [], y: [], z: [[]]}"
)
},
"options": {
"type": "object",
"description": (
"Optional settings: title, x_label, y_label, color, "
"showlegend, height, width, horizontal (for bar)"
)
}
},
"required": ["chart_type", "data"]
}
))
tools.append(Tool(
name="chart_configure_interaction",
description=(
"Configure interactions on an existing chart. "
"Add hover templates, enable click data capture, selection modes, "
"and zoom behavior for Dash callback integration."
),
inputSchema={
"type": "object",
"properties": {
"figure": {
"type": "object",
"description": "Plotly figure JSON to modify"
},
"interactions": {
"type": "object",
"description": (
"Interaction config: hover_template (string), "
"click_data (bool), selection ('box'|'lasso'), zoom (bool)"
)
}
},
"required": ["figure", "interactions"]
}
))
# Chart export tool (Issue #247)
tools.append(Tool(
name="chart_export",
description=(
"Export a Plotly chart to static image format (PNG, SVG, PDF). "
"Requires kaleido package. Returns base64 image data or saves to file."
),
inputSchema={
"type": "object",
"properties": {
"figure": {
"type": "object",
"description": "Plotly figure JSON to export"
},
"format": {
"type": "string",
"enum": ["png", "svg", "pdf"],
"description": "Output format (default: png)"
},
"width": {
"type": "integer",
"description": "Image width in pixels (default: 1200)"
},
"height": {
"type": "integer",
"description": "Image height in pixels (default: 800)"
},
"scale": {
"type": "number",
"description": "Resolution scale factor (default: 2 for retina)"
},
"output_path": {
"type": "string",
"description": "Optional file path to save image"
}
},
"required": ["figure"]
}
))
# Layout tools (Issue #174)
tools.append(Tool(
name="layout_create",
description=(
"Create a new dashboard layout container. "
"Templates available: dashboard, report, form, blank. "
"Returns layout reference for use with other layout tools."
),
inputSchema={
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Unique name for the layout"
},
"template": {
"type": "string",
"enum": ["dashboard", "report", "form", "blank"],
"description": "Layout template to use (default: blank)"
}
},
"required": ["name"]
}
))
tools.append(Tool(
name="layout_add_filter",
description=(
"Add a filter control to a layout. "
"Filter types: dropdown, multi_select, date_range, date, search, "
"checkbox_group, radio_group, slider, range_slider."
),
inputSchema={
"type": "object",
"properties": {
"layout_ref": {
"type": "string",
"description": "Layout name to add filter to"
},
"filter_type": {
"type": "string",
"enum": ["dropdown", "multi_select", "date_range", "date",
"search", "checkbox_group", "radio_group", "slider", "range_slider"],
"description": "Type of filter control"
},
"options": {
"type": "object",
"description": (
"Filter options: label, data (for dropdown), placeholder, "
"position (section name), value, etc."
)
}
},
"required": ["layout_ref", "filter_type", "options"]
}
))
tools.append(Tool(
name="layout_set_grid",
description=(
"Configure the grid system for a layout. "
"Uses DMC Grid component patterns with 12 or 24 column system."
),
inputSchema={
"type": "object",
"properties": {
"layout_ref": {
"type": "string",
"description": "Layout name to configure"
},
"grid": {
"type": "object",
"description": (
"Grid config: cols (1-24), spacing (xs|sm|md|lg|xl), "
"breakpoints ({xs: cols, sm: cols, ...}), gutter"
)
}
},
"required": ["layout_ref", "grid"]
}
))
# Responsive breakpoints tool (Issue #249)
tools.append(Tool(
name="layout_set_breakpoints",
description=(
"Configure responsive breakpoints for a layout. "
"Supports xs, sm, md, lg, xl breakpoints with mobile-first approach. "
"Each breakpoint can define cols, spacing, and other grid properties."
),
inputSchema={
"type": "object",
"properties": {
"layout_ref": {
"type": "string",
"description": "Layout name to configure"
},
"breakpoints": {
"type": "object",
"description": (
"Breakpoint config: {xs: {cols, spacing}, sm: {...}, md: {...}, lg: {...}, xl: {...}}"
)
},
"mobile_first": {
"type": "boolean",
"description": "Use mobile-first (min-width) media queries (default: true)"
}
},
"required": ["layout_ref", "breakpoints"]
}
))
# Theme tools (Issue #175)
tools.append(Tool(
name="theme_create",
description=(
"Create a new design theme with tokens. "
"Tokens include colors, spacing, typography, radii. "
"Missing tokens are filled from defaults."
),
inputSchema={
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Unique theme name"
},
"tokens": {
"type": "object",
"description": (
"Design tokens: colors (primary, background, text), "
"spacing (xs-xl), typography (fontFamily, fontSize), radii (sm-xl)"
)
}
},
"required": ["name", "tokens"]
}
))
tools.append(Tool(
name="theme_extend",
description=(
"Create a new theme by extending an existing one. "
"Only specify the tokens you want to override."
),
inputSchema={
"type": "object",
"properties": {
"base_theme": {
"type": "string",
"description": "Theme to extend (e.g., 'default')"
},
"overrides": {
"type": "object",
"description": "Token overrides to apply"
},
"new_name": {
"type": "string",
"description": "Name for the new theme (optional)"
}
},
"required": ["base_theme", "overrides"]
}
))
tools.append(Tool(
name="theme_validate",
description=(
"Validate a theme for completeness. "
"Checks for required tokens and common issues."
),
inputSchema={
"type": "object",
"properties": {
"theme_name": {
"type": "string",
"description": "Theme to validate"
}
},
"required": ["theme_name"]
}
))
tools.append(Tool(
name="theme_export_css",
description=(
"Export a theme as CSS custom properties. "
"Generates :root CSS variables for all tokens."
),
inputSchema={
"type": "object",
"properties": {
"theme_name": {
"type": "string",
"description": "Theme to export"
}
},
"required": ["theme_name"]
}
))
# Page tools (Issue #176)
tools.append(Tool(
name="page_create",
description=(
"Create a new page for a multi-page Dash application. "
"Defines page routing and can link to a layout."
),
inputSchema={
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Unique page name (identifier)"
},
"path": {
"type": "string",
"description": "URL path (e.g., '/', '/settings')"
},
"layout_ref": {
"type": "string",
"description": "Optional layout reference to use"
},
"title": {
"type": "string",
"description": "Page title (defaults to name)"
}
},
"required": ["name", "path"]
}
))
tools.append(Tool(
name="page_add_navbar",
description=(
"Generate navigation component linking pages. "
"Creates top or side navigation with DMC components."
),
inputSchema={
"type": "object",
"properties": {
"pages": {
"type": "array",
"items": {"type": "string"},
"description": "List of page names to include"
},
"options": {
"type": "object",
"description": (
"Navigation options: position (top|left|right), "
"brand (app name), collapsible (bool)"
)
}
},
"required": ["pages"]
}
))
tools.append(Tool(
name="page_set_auth",
description=(
"Configure authentication for a page. "
"Sets auth requirements, roles, and redirect behavior."
),
inputSchema={
"type": "object",
"properties": {
"page_ref": {
"type": "string",
"description": "Page name to configure"
},
"auth_config": {
"type": "object",
"description": (
"Auth config: type (none|basic|oauth|custom), "
"required (bool), roles (array), redirect (path)"
)
}
},
"required": ["page_ref", "auth_config"]
}
))
# Accessibility tools (Issue #248)
tools.append(Tool(
name="accessibility_validate_colors",
description=(
"Validate colors for color blind accessibility. "
"Checks contrast ratios for deuteranopia, protanopia, tritanopia. "
"Returns issues, simulations, and accessible palette suggestions."
),
inputSchema={
"type": "object",
"properties": {
"colors": {
"type": "array",
"items": {"type": "string"},
"description": "List of hex colors to validate (e.g., ['#228be6', '#40c057'])"
},
"check_types": {
"type": "array",
"items": {"type": "string"},
"description": "Color blindness types to check: deuteranopia, protanopia, tritanopia (default: all)"
},
"min_contrast_ratio": {
"type": "number",
"description": "Minimum WCAG contrast ratio (default: 4.5 for AA)"
}
},
"required": ["colors"]
}
))
tools.append(Tool(
name="accessibility_validate_theme",
description=(
"Validate a theme's colors for accessibility. "
"Extracts all colors from theme tokens and checks for color blind safety."
),
inputSchema={
"type": "object",
"properties": {
"theme_name": {
"type": "string",
"description": "Theme name to validate"
}
},
"required": ["theme_name"]
}
))
tools.append(Tool(
name="accessibility_suggest_alternative",
description=(
"Suggest accessible alternative colors for a given color. "
"Provides alternatives optimized for specific color blindness types."
),
inputSchema={
"type": "object",
"properties": {
"color": {
"type": "string",
"description": "Hex color to find alternatives for"
},
"deficiency_type": {
"type": "string",
"enum": ["deuteranopia", "protanopia", "tritanopia"],
"description": "Color blindness type to optimize for"
}
},
"required": ["color", "deficiency_type"]
}
))
return tools
@self.server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
"""Handle tool invocation."""
try:
# DMC validation tools
if name == "list_components":
result = await self.dmc_tools.list_components(
category=arguments.get('category')
)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "get_component_props":
component = arguments.get('component')
if not component:
return [TextContent(
type="text",
text=json.dumps({"error": "component is required"}, indent=2)
)]
result = await self.dmc_tools.get_component_props(component)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "validate_component":
component = arguments.get('component')
props = arguments.get('props', {})
if not component:
return [TextContent(
type="text",
text=json.dumps({"error": "component is required"}, indent=2)
)]
result = await self.dmc_tools.validate_component(component, props)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
# Chart tools
elif name == "chart_create":
chart_type = arguments.get('chart_type')
data = arguments.get('data', {})
options = arguments.get('options', {})
if not chart_type:
return [TextContent(
type="text",
text=json.dumps({"error": "chart_type is required"}, indent=2)
)]
result = await self.chart_tools.chart_create(chart_type, data, options)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "chart_configure_interaction":
figure = arguments.get('figure')
interactions = arguments.get('interactions', {})
if not figure:
return [TextContent(
type="text",
text=json.dumps({"error": "figure is required"}, indent=2)
)]
result = await self.chart_tools.chart_configure_interaction(figure, interactions)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "chart_export":
figure = arguments.get('figure')
if not figure:
return [TextContent(
type="text",
text=json.dumps({"error": "figure is required"}, indent=2)
)]
result = await self.chart_tools.chart_export(
figure=figure,
format=arguments.get('format', 'png'),
width=arguments.get('width'),
height=arguments.get('height'),
scale=arguments.get('scale', 2.0),
output_path=arguments.get('output_path')
)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
# Layout tools
elif name == "layout_create":
layout_name = arguments.get('name')
template = arguments.get('template')
if not layout_name:
return [TextContent(
type="text",
text=json.dumps({"error": "name is required"}, indent=2)
)]
result = await self.layout_tools.layout_create(layout_name, template)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "layout_add_filter":
layout_ref = arguments.get('layout_ref')
filter_type = arguments.get('filter_type')
options = arguments.get('options', {})
if not layout_ref or not filter_type:
return [TextContent(
type="text",
text=json.dumps({"error": "layout_ref and filter_type are required"}, indent=2)
)]
result = await self.layout_tools.layout_add_filter(layout_ref, filter_type, options)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "layout_set_grid":
layout_ref = arguments.get('layout_ref')
grid = arguments.get('grid', {})
if not layout_ref:
return [TextContent(
type="text",
text=json.dumps({"error": "layout_ref is required"}, indent=2)
)]
result = await self.layout_tools.layout_set_grid(layout_ref, grid)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "layout_set_breakpoints":
layout_ref = arguments.get('layout_ref')
breakpoints = arguments.get('breakpoints', {})
mobile_first = arguments.get('mobile_first', True)
if not layout_ref:
return [TextContent(
type="text",
text=json.dumps({"error": "layout_ref is required"}, indent=2)
)]
result = await self.layout_tools.layout_set_breakpoints(
layout_ref, breakpoints, mobile_first
)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
# Theme tools
elif name == "theme_create":
theme_name = arguments.get('name')
tokens = arguments.get('tokens', {})
if not theme_name:
return [TextContent(
type="text",
text=json.dumps({"error": "name is required"}, indent=2)
)]
result = await self.theme_tools.theme_create(theme_name, tokens)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "theme_extend":
base_theme = arguments.get('base_theme')
overrides = arguments.get('overrides', {})
new_name = arguments.get('new_name')
if not base_theme:
return [TextContent(
type="text",
text=json.dumps({"error": "base_theme is required"}, indent=2)
)]
result = await self.theme_tools.theme_extend(base_theme, overrides, new_name)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "theme_validate":
theme_name = arguments.get('theme_name')
if not theme_name:
return [TextContent(
type="text",
text=json.dumps({"error": "theme_name is required"}, indent=2)
)]
result = await self.theme_tools.theme_validate(theme_name)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "theme_export_css":
theme_name = arguments.get('theme_name')
if not theme_name:
return [TextContent(
type="text",
text=json.dumps({"error": "theme_name is required"}, indent=2)
)]
result = await self.theme_tools.theme_export_css(theme_name)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
# Page tools
elif name == "page_create":
page_name = arguments.get('name')
path = arguments.get('path')
layout_ref = arguments.get('layout_ref')
title = arguments.get('title')
if not page_name or not path:
return [TextContent(
type="text",
text=json.dumps({"error": "name and path are required"}, indent=2)
)]
result = await self.page_tools.page_create(page_name, path, layout_ref, title)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "page_add_navbar":
pages = arguments.get('pages', [])
options = arguments.get('options', {})
if not pages:
return [TextContent(
type="text",
text=json.dumps({"error": "pages list is required"}, indent=2)
)]
result = await self.page_tools.page_add_navbar(pages, options)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "page_set_auth":
page_ref = arguments.get('page_ref')
auth_config = arguments.get('auth_config', {})
if not page_ref:
return [TextContent(
type="text",
text=json.dumps({"error": "page_ref is required"}, indent=2)
)]
result = await self.page_tools.page_set_auth(page_ref, auth_config)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
# Accessibility tools
elif name == "accessibility_validate_colors":
colors = arguments.get('colors')
if not colors:
return [TextContent(
type="text",
text=json.dumps({"error": "colors list is required"}, indent=2)
)]
result = await self.accessibility_tools.accessibility_validate_colors(
colors=colors,
check_types=arguments.get('check_types'),
min_contrast_ratio=arguments.get('min_contrast_ratio', 4.5)
)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "accessibility_validate_theme":
theme_name = arguments.get('theme_name')
if not theme_name:
return [TextContent(
type="text",
text=json.dumps({"error": "theme_name is required"}, indent=2)
)]
result = await self.accessibility_tools.accessibility_validate_theme(theme_name)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "accessibility_suggest_alternative":
color = arguments.get('color')
deficiency_type = arguments.get('deficiency_type')
if not color or not deficiency_type:
return [TextContent(
type="text",
text=json.dumps({"error": "color and deficiency_type are required"}, indent=2)
)]
result = await self.accessibility_tools.accessibility_suggest_alternative(
color, deficiency_type
)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
raise ValueError(f"Unknown tool: {name}")
except Exception as e:
logger.error(f"Tool {name} failed: {e}")
return [TextContent(
type="text",
text=json.dumps({"error": str(e)}, indent=2)
)]
async def run(self):
"""Run the MCP server"""
await self.initialize()
self.setup_tools()
async with stdio_server() as (read_stream, write_stream):
await self.server.run(
read_stream,
write_stream,
self.server.create_initialization_options()
)
async def main():
"""Main entry point"""
server = VizPlatformMCPServer()
await server.run()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -1,259 +0,0 @@
"""
Theme storage and persistence for viz-platform.
Handles saving/loading themes from user and project locations.
"""
import json
import logging
from pathlib import Path
from typing import Dict, List, Optional, Any
logger = logging.getLogger(__name__)
# Default theme based on Mantine defaults
DEFAULT_THEME = {
"name": "default",
"version": "1.0.0",
"tokens": {
"colors": {
"primary": "#228be6",
"secondary": "#868e96",
"success": "#40c057",
"warning": "#fab005",
"error": "#fa5252",
"info": "#15aabf",
"background": {
"base": "#ffffff",
"subtle": "#f8f9fa",
"dark": "#212529"
},
"text": {
"primary": "#212529",
"secondary": "#495057",
"muted": "#868e96",
"inverse": "#ffffff"
},
"border": "#dee2e6"
},
"spacing": {
"xs": "4px",
"sm": "8px",
"md": "16px",
"lg": "24px",
"xl": "32px"
},
"typography": {
"fontFamily": "Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif",
"fontFamilyMono": "ui-monospace, SFMono-Regular, Menlo, Monaco, monospace",
"fontSize": {
"xs": "12px",
"sm": "14px",
"md": "16px",
"lg": "18px",
"xl": "20px"
},
"fontWeight": {
"normal": 400,
"medium": 500,
"semibold": 600,
"bold": 700
},
"lineHeight": {
"tight": 1.25,
"normal": 1.5,
"relaxed": 1.75
}
},
"radii": {
"none": "0px",
"sm": "4px",
"md": "8px",
"lg": "16px",
"xl": "24px",
"full": "9999px"
},
"shadows": {
"none": "none",
"sm": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
"md": "0 4px 6px -1px rgb(0 0 0 / 0.1)",
"lg": "0 10px 15px -3px rgb(0 0 0 / 0.1)",
"xl": "0 20px 25px -5px rgb(0 0 0 / 0.1)"
},
"transitions": {
"fast": "150ms",
"normal": "300ms",
"slow": "500ms"
}
}
}
# Required token categories for validation
REQUIRED_TOKEN_CATEGORIES = ["colors", "spacing", "typography", "radii"]
class ThemeStore:
"""
Store and manage design themes.
Handles persistence to user-level and project-level locations.
"""
def __init__(self, project_dir: Optional[Path] = None):
"""
Initialize theme store.
Args:
project_dir: Project directory for project-level themes
"""
self.project_dir = project_dir
self._themes: Dict[str, Dict[str, Any]] = {}
self._active_theme: Optional[str] = None
# Load default theme
self._themes["default"] = DEFAULT_THEME.copy()
@property
def user_themes_dir(self) -> Path:
"""User-level themes directory."""
return Path.home() / ".config" / "claude" / "themes"
@property
def project_themes_dir(self) -> Optional[Path]:
"""Project-level themes directory."""
if self.project_dir:
return self.project_dir / ".viz-platform" / "themes"
return None
def load_themes(self) -> int:
"""
Load themes from user and project directories.
Project themes take precedence over user themes.
Returns:
Number of themes loaded
"""
count = 0
# Load user themes
if self.user_themes_dir.exists():
for theme_file in self.user_themes_dir.glob("*.json"):
try:
with open(theme_file, 'r') as f:
theme = json.load(f)
name = theme.get('name', theme_file.stem)
self._themes[name] = theme
count += 1
logger.debug(f"Loaded user theme: {name}")
except Exception as e:
logger.warning(f"Failed to load theme {theme_file}: {e}")
# Load project themes (override user themes)
if self.project_themes_dir and self.project_themes_dir.exists():
for theme_file in self.project_themes_dir.glob("*.json"):
try:
with open(theme_file, 'r') as f:
theme = json.load(f)
name = theme.get('name', theme_file.stem)
self._themes[name] = theme
count += 1
logger.debug(f"Loaded project theme: {name}")
except Exception as e:
logger.warning(f"Failed to load theme {theme_file}: {e}")
return count
def save_theme(
self,
theme: Dict[str, Any],
location: str = "project"
) -> Path:
"""
Save a theme to disk.
Args:
theme: Theme dict to save
location: "user" or "project"
Returns:
Path where theme was saved
"""
name = theme.get('name', 'unnamed')
if location == "user":
target_dir = self.user_themes_dir
else:
target_dir = self.project_themes_dir
if not target_dir:
target_dir = self.user_themes_dir
target_dir.mkdir(parents=True, exist_ok=True)
theme_path = target_dir / f"{name}.json"
with open(theme_path, 'w') as f:
json.dump(theme, f, indent=2)
# Update in-memory store
self._themes[name] = theme
return theme_path
def get_theme(self, name: str) -> Optional[Dict[str, Any]]:
"""Get a theme by name."""
return self._themes.get(name)
def list_themes(self) -> List[str]:
"""List all available theme names."""
return list(self._themes.keys())
def set_active_theme(self, name: str) -> bool:
"""
Set the active theme.
Args:
name: Theme name to activate
Returns:
True if theme was activated
"""
if name in self._themes:
self._active_theme = name
return True
return False
def get_active_theme(self) -> Optional[Dict[str, Any]]:
"""Get the currently active theme."""
if self._active_theme:
return self._themes.get(self._active_theme)
return None
def delete_theme(self, name: str) -> bool:
"""
Delete a theme.
Args:
name: Theme name to delete
Returns:
True if theme was deleted
"""
if name == "default":
return False # Cannot delete default theme
if name in self._themes:
del self._themes[name]
# Remove file if exists
for themes_dir in [self.user_themes_dir, self.project_themes_dir]:
if themes_dir and themes_dir.exists():
theme_path = themes_dir / f"{name}.json"
if theme_path.exists():
theme_path.unlink()
if self._active_theme == name:
self._active_theme = None
return True
return False

View File

@@ -1,391 +0,0 @@
"""
Theme management tools for viz-platform.
Provides design token-based theming system for consistent visual styling.
"""
import copy
import logging
from typing import Dict, List, Optional, Any
from .theme_store import ThemeStore, DEFAULT_THEME, REQUIRED_TOKEN_CATEGORIES
logger = logging.getLogger(__name__)
class ThemeTools:
"""
Design token-based theming tools.
Creates and manages themes that integrate with DMC and Plotly.
"""
def __init__(self, store: Optional[ThemeStore] = None):
"""
Initialize theme tools.
Args:
store: Optional ThemeStore for persistence
"""
self.store = store or ThemeStore()
async def theme_create(
self,
name: str,
tokens: Dict[str, Any]
) -> Dict[str, Any]:
"""
Create a new theme with design tokens.
Args:
name: Unique theme name
tokens: Design tokens dict with colors, spacing, typography, radii
Returns:
Dict with:
- name: Theme name
- tokens: Full token set (merged with defaults)
- validation: Validation results
"""
# Check for name collision
if self.store.get_theme(name) and name != "default":
return {
"error": f"Theme '{name}' already exists. Use theme_extend to modify it.",
"name": name
}
# Start with default tokens and merge provided ones
theme_tokens = copy.deepcopy(DEFAULT_THEME["tokens"])
theme_tokens = self._deep_merge(theme_tokens, tokens)
# Create theme object
theme = {
"name": name,
"version": "1.0.0",
"tokens": theme_tokens
}
# Validate the theme
validation = self._validate_tokens(theme_tokens)
# Save to store
self.store._themes[name] = theme
return {
"name": name,
"tokens": theme_tokens,
"validation": validation,
"complete": validation["complete"]
}
async def theme_extend(
self,
base_theme: str,
overrides: Dict[str, Any],
new_name: Optional[str] = None
) -> Dict[str, Any]:
"""
Create a new theme by extending an existing one.
Args:
base_theme: Name of theme to extend
overrides: Token overrides to apply
new_name: Optional name for the new theme (defaults to base_theme_extended)
Returns:
Dict with the new theme or error
"""
# Get base theme
base = self.store.get_theme(base_theme)
if not base:
available = self.store.list_themes()
return {
"error": f"Base theme '{base_theme}' not found. Available: {available}",
"name": None
}
# Determine new name
name = new_name or f"{base_theme}_extended"
# Check for collision
if self.store.get_theme(name) and name != base_theme:
return {
"error": f"Theme '{name}' already exists. Choose a different name.",
"name": name
}
# Merge tokens
theme_tokens = copy.deepcopy(base.get("tokens", {}))
theme_tokens = self._deep_merge(theme_tokens, overrides)
# Create theme
theme = {
"name": name,
"version": "1.0.0",
"extends": base_theme,
"tokens": theme_tokens
}
# Validate
validation = self._validate_tokens(theme_tokens)
# Save to store
self.store._themes[name] = theme
return {
"name": name,
"extends": base_theme,
"tokens": theme_tokens,
"validation": validation,
"complete": validation["complete"]
}
async def theme_validate(self, theme_name: str) -> Dict[str, Any]:
"""
Validate a theme for completeness.
Args:
theme_name: Theme name to validate
Returns:
Dict with:
- valid: bool
- complete: bool (all optional tokens present)
- missing: List of missing required tokens
- warnings: List of warnings
"""
theme = self.store.get_theme(theme_name)
if not theme:
available = self.store.list_themes()
return {
"error": f"Theme '{theme_name}' not found. Available: {available}",
"valid": False
}
tokens = theme.get("tokens", {})
validation = self._validate_tokens(tokens)
return {
"theme_name": theme_name,
"valid": validation["valid"],
"complete": validation["complete"],
"missing_required": validation["missing_required"],
"missing_optional": validation["missing_optional"],
"warnings": validation["warnings"]
}
async def theme_export_css(self, theme_name: str) -> Dict[str, Any]:
"""
Export a theme as CSS custom properties.
Args:
theme_name: Theme name to export
Returns:
Dict with:
- css: CSS custom properties string
- variables: List of variable names
"""
theme = self.store.get_theme(theme_name)
if not theme:
available = self.store.list_themes()
return {
"error": f"Theme '{theme_name}' not found. Available: {available}",
"css": None
}
tokens = theme.get("tokens", {})
css_vars = []
var_names = []
# Convert tokens to CSS custom properties
css_vars.append(f"/* Theme: {theme_name} */")
css_vars.append(":root {")
# Colors
colors = tokens.get("colors", {})
css_vars.append(" /* Colors */")
for key, value in self._flatten_tokens(colors, "color").items():
var_name = f"--{key}"
css_vars.append(f" {var_name}: {value};")
var_names.append(var_name)
# Spacing
spacing = tokens.get("spacing", {})
css_vars.append("\n /* Spacing */")
for key, value in spacing.items():
var_name = f"--spacing-{key}"
css_vars.append(f" {var_name}: {value};")
var_names.append(var_name)
# Typography
typography = tokens.get("typography", {})
css_vars.append("\n /* Typography */")
for key, value in self._flatten_tokens(typography, "font").items():
var_name = f"--{key}"
css_vars.append(f" {var_name}: {value};")
var_names.append(var_name)
# Radii
radii = tokens.get("radii", {})
css_vars.append("\n /* Border Radius */")
for key, value in radii.items():
var_name = f"--radius-{key}"
css_vars.append(f" {var_name}: {value};")
var_names.append(var_name)
# Shadows
shadows = tokens.get("shadows", {})
if shadows:
css_vars.append("\n /* Shadows */")
for key, value in shadows.items():
var_name = f"--shadow-{key}"
css_vars.append(f" {var_name}: {value};")
var_names.append(var_name)
# Transitions
transitions = tokens.get("transitions", {})
if transitions:
css_vars.append("\n /* Transitions */")
for key, value in transitions.items():
var_name = f"--transition-{key}"
css_vars.append(f" {var_name}: {value};")
var_names.append(var_name)
css_vars.append("}")
css_content = "\n".join(css_vars)
return {
"theme_name": theme_name,
"css": css_content,
"variable_count": len(var_names),
"variables": var_names
}
async def theme_list(self) -> Dict[str, Any]:
"""
List all available themes.
Returns:
Dict with theme names and active theme
"""
themes = self.store.list_themes()
active = self.store._active_theme
theme_info = {}
for name in themes:
theme = self.store.get_theme(name)
theme_info[name] = {
"extends": theme.get("extends"),
"version": theme.get("version", "1.0.0")
}
return {
"themes": theme_info,
"active_theme": active,
"count": len(themes)
}
async def theme_activate(self, theme_name: str) -> Dict[str, Any]:
"""
Set the active theme.
Args:
theme_name: Theme to activate
Returns:
Dict with activation status
"""
if self.store.set_active_theme(theme_name):
return {
"active_theme": theme_name,
"success": True
}
return {
"error": f"Theme '{theme_name}' not found.",
"success": False
}
def _validate_tokens(self, tokens: Dict[str, Any]) -> Dict[str, Any]:
"""Validate token structure and completeness."""
missing_required = []
missing_optional = []
warnings = []
# Check required categories
for category in REQUIRED_TOKEN_CATEGORIES:
if category not in tokens:
missing_required.append(category)
# Check colors structure
colors = tokens.get("colors", {})
required_colors = ["primary", "background", "text"]
for color in required_colors:
if color not in colors:
missing_required.append(f"colors.{color}")
# Check spacing
spacing = tokens.get("spacing", {})
required_spacing = ["xs", "sm", "md", "lg"]
for size in required_spacing:
if size not in spacing:
missing_optional.append(f"spacing.{size}")
# Check typography
typography = tokens.get("typography", {})
if "fontFamily" not in typography:
missing_optional.append("typography.fontFamily")
if "fontSize" not in typography:
missing_optional.append("typography.fontSize")
# Check radii
radii = tokens.get("radii", {})
if "sm" not in radii and "md" not in radii:
missing_optional.append("radii.sm or radii.md")
# Warnings for common issues
if "shadows" not in tokens:
warnings.append("No shadows defined - components may have no elevation")
if "transitions" not in tokens:
warnings.append("No transitions defined - animations will use defaults")
return {
"valid": len(missing_required) == 0,
"complete": len(missing_required) == 0 and len(missing_optional) == 0,
"missing_required": missing_required,
"missing_optional": missing_optional,
"warnings": warnings
}
def _deep_merge(
self,
base: Dict[str, Any],
override: Dict[str, Any]
) -> Dict[str, Any]:
"""Deep merge two dictionaries."""
result = copy.deepcopy(base)
for key, value in override.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = self._deep_merge(result[key], value)
else:
result[key] = value
return result
def _flatten_tokens(
self,
tokens: Dict[str, Any],
prefix: str
) -> Dict[str, str]:
"""Flatten nested token dict for CSS export."""
result = {}
for key, value in tokens.items():
if isinstance(value, dict):
nested = self._flatten_tokens(value, f"{prefix}-{key}")
result.update(nested)
else:
result[f"{prefix}-{key}"] = str(value)
return result

View File

@@ -1,45 +0,0 @@
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "viz-platform-mcp"
version = "1.0.0"
description = "MCP Server for visualization with Dash Mantine Components validation and theming"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.10"
authors = [
{name = "Leo Miranda"}
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"mcp>=0.9.0",
"plotly>=5.18.0",
"dash>=2.14.0",
"dash-mantine-components>=2.0.0",
"python-dotenv>=1.0.0",
"pydantic>=2.5.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.4.3",
"pytest-asyncio>=0.23.0",
]
[tool.setuptools.packages.find]
where = ["."]
include = ["mcp_server*"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

View File

@@ -1,668 +0,0 @@
{
"version": "2.5.1",
"generated": "2026-01-26",
"mantine_version": "7.x",
"categories": {
"buttons": ["Button", "ButtonGroup", "ActionIcon", "ActionIconGroup", "CopyButton", "CloseButton", "UnstyledButton"],
"inputs": [
"TextInput", "PasswordInput", "NumberInput", "Textarea", "Select", "MultiSelect",
"Checkbox", "CheckboxGroup", "CheckboxCard", "Switch", "Radio", "RadioGroup", "RadioCard",
"Slider", "RangeSlider", "ColorInput", "ColorPicker", "Autocomplete", "TagsInput",
"PinInput", "Rating", "SegmentedControl", "Chip", "ChipGroup", "JsonInput",
"NativeSelect", "FileInput", "Combobox"
],
"navigation": ["Anchor", "Breadcrumbs", "Burger", "NavLink", "Pagination", "Stepper", "Tabs", "TabsList", "TabsTab", "TabsPanel"],
"feedback": ["Alert", "Loader", "Notification", "NotificationContainer", "Progress", "RingProgress", "Skeleton"],
"overlays": ["Modal", "Drawer", "DrawerStack", "Popover", "HoverCard", "Tooltip", "FloatingTooltip", "Menu", "MenuTarget", "MenuDropdown", "MenuItem", "Affix"],
"typography": ["Text", "Title", "Highlight", "Mark", "Code", "CodeHighlight", "Blockquote", "List", "ListItem", "Kbd"],
"layout": [
"AppShell", "AppShellHeader", "AppShellNavbar", "AppShellAside", "AppShellFooter", "AppShellMain", "AppShellSection",
"Container", "Center", "Stack", "Group", "Flex", "Grid", "GridCol", "SimpleGrid",
"Paper", "Card", "CardSection", "Box", "Space", "Divider", "AspectRatio", "ScrollArea"
],
"data_display": [
"Accordion", "AccordionItem", "AccordionControl", "AccordionPanel",
"Avatar", "AvatarGroup", "Badge", "Image", "BackgroundImage",
"Indicator", "Spoiler", "Table", "ThemeIcon", "Timeline", "TimelineItem", "Tree"
],
"charts": ["AreaChart", "BarChart", "LineChart", "PieChart", "DonutChart", "RadarChart", "ScatterChart", "BubbleChart", "CompositeChart", "Sparkline"],
"dates": ["DatePicker", "DateTimePicker", "DateInput", "DatePickerInput", "MonthPicker", "YearPicker", "TimePicker", "TimeInput", "Calendar", "MiniCalendar", "DatesProvider"]
},
"components": {
"Button": {
"description": "Button component for user interactions",
"props": {
"children": {"type": "any", "description": "Button content"},
"variant": {"type": "string", "enum": ["filled", "light", "outline", "transparent", "white", "subtle", "default", "gradient"], "default": "filled"},
"color": {"type": "string", "default": "blue", "description": "Key of theme.colors or CSS color"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl", "compact-xs", "compact-sm", "compact-md", "compact-lg", "compact-xl"], "default": "sm"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"disabled": {"type": "boolean", "default": false},
"loading": {"type": "boolean", "default": false},
"loaderProps": {"type": "object"},
"leftSection": {"type": "any", "description": "Content on the left side of label"},
"rightSection": {"type": "any", "description": "Content on the right side of label"},
"fullWidth": {"type": "boolean", "default": false},
"gradient": {"type": "object", "description": "Gradient for gradient variant"},
"justify": {"type": "string", "enum": ["center", "start", "end", "space-between"], "default": "center"},
"autoContrast": {"type": "boolean", "default": false},
"n_clicks": {"type": "integer", "default": 0, "description": "Dash callback trigger"}
}
},
"ActionIcon": {
"description": "Icon button without text label",
"props": {
"children": {"type": "any", "required": true, "description": "Icon element"},
"variant": {"type": "string", "enum": ["filled", "light", "outline", "transparent", "white", "subtle", "default", "gradient"], "default": "subtle"},
"color": {"type": "string", "default": "gray"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"disabled": {"type": "boolean", "default": false},
"loading": {"type": "boolean", "default": false},
"autoContrast": {"type": "boolean", "default": false},
"n_clicks": {"type": "integer", "default": 0}
}
},
"TextInput": {
"description": "Text input field",
"props": {
"value": {"type": "string", "default": ""},
"placeholder": {"type": "string"},
"label": {"type": "any"},
"description": {"type": "any"},
"error": {"type": "any"},
"disabled": {"type": "boolean", "default": false},
"required": {"type": "boolean", "default": false},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"variant": {"type": "string", "enum": ["default", "filled", "unstyled"], "default": "default"},
"leftSection": {"type": "any"},
"rightSection": {"type": "any"},
"withAsterisk": {"type": "boolean", "default": false},
"debounce": {"type": "integer", "description": "Debounce delay in ms"},
"leftSectionPointerEvents": {"type": "string", "enum": ["none", "all"], "default": "none"},
"rightSectionPointerEvents": {"type": "string", "enum": ["none", "all"], "default": "none"}
}
},
"NumberInput": {
"description": "Numeric input with optional controls",
"props": {
"value": {"type": "number"},
"placeholder": {"type": "string"},
"label": {"type": "any"},
"description": {"type": "any"},
"error": {"type": "any"},
"disabled": {"type": "boolean", "default": false},
"required": {"type": "boolean", "default": false},
"min": {"type": "number"},
"max": {"type": "number"},
"step": {"type": "number", "default": 1},
"hideControls": {"type": "boolean", "default": false},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"allowNegative": {"type": "boolean", "default": true},
"allowDecimal": {"type": "boolean", "default": true},
"clampBehavior": {"type": "string", "enum": ["strict", "blur", "none"], "default": "blur"},
"decimalScale": {"type": "integer"},
"fixedDecimalScale": {"type": "boolean", "default": false},
"thousandSeparator": {"type": "string"},
"decimalSeparator": {"type": "string"},
"prefix": {"type": "string"},
"suffix": {"type": "string"}
}
},
"Select": {
"description": "Dropdown select input",
"props": {
"value": {"type": "string"},
"data": {"type": "array", "required": true, "description": "Array of options: strings or {value, label} objects"},
"placeholder": {"type": "string"},
"label": {"type": "any"},
"description": {"type": "any"},
"error": {"type": "any"},
"disabled": {"type": "boolean", "default": false},
"required": {"type": "boolean", "default": false},
"searchable": {"type": "boolean", "default": false},
"clearable": {"type": "boolean", "default": false},
"nothingFoundMessage": {"type": "string"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"maxDropdownHeight": {"type": "number", "default": 250},
"allowDeselect": {"type": "boolean", "default": true},
"checkIconPosition": {"type": "string", "enum": ["left", "right"], "default": "left"},
"comboboxProps": {"type": "object"},
"withScrollArea": {"type": "boolean", "default": true}
}
},
"MultiSelect": {
"description": "Multiple selection dropdown",
"props": {
"value": {"type": "array", "default": []},
"data": {"type": "array", "required": true},
"placeholder": {"type": "string"},
"label": {"type": "any"},
"description": {"type": "any"},
"error": {"type": "any"},
"disabled": {"type": "boolean", "default": false},
"required": {"type": "boolean", "default": false},
"searchable": {"type": "boolean", "default": false},
"clearable": {"type": "boolean", "default": false},
"maxValues": {"type": "integer"},
"hidePickedOptions": {"type": "boolean", "default": false},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"maxDropdownHeight": {"type": "number", "default": 250},
"withCheckIcon": {"type": "boolean", "default": true}
}
},
"Checkbox": {
"description": "Checkbox input",
"props": {
"checked": {"type": "boolean", "default": false},
"label": {"type": "any"},
"description": {"type": "any"},
"error": {"type": "any"},
"disabled": {"type": "boolean", "default": false},
"indeterminate": {"type": "boolean", "default": false},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"color": {"type": "string", "default": "blue"},
"labelPosition": {"type": "string", "enum": ["left", "right"], "default": "right"},
"autoContrast": {"type": "boolean", "default": false},
"icon": {"type": "any"},
"iconColor": {"type": "string"}
}
},
"Switch": {
"description": "Toggle switch input",
"props": {
"checked": {"type": "boolean", "default": false},
"label": {"type": "any"},
"description": {"type": "any"},
"error": {"type": "any"},
"disabled": {"type": "boolean", "default": false},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "xl"},
"color": {"type": "string", "default": "blue"},
"onLabel": {"type": "any"},
"offLabel": {"type": "any"},
"thumbIcon": {"type": "any"},
"labelPosition": {"type": "string", "enum": ["left", "right"], "default": "right"}
}
},
"Slider": {
"description": "Slider input for numeric values",
"props": {
"value": {"type": "number"},
"min": {"type": "number", "default": 0},
"max": {"type": "number", "default": 100},
"step": {"type": "number", "default": 1},
"label": {"type": "any"},
"disabled": {"type": "boolean", "default": false},
"marks": {"type": "array"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "xl"},
"color": {"type": "string", "default": "blue"},
"showLabelOnHover": {"type": "boolean", "default": true},
"labelAlwaysOn": {"type": "boolean", "default": false},
"thumbLabel": {"type": "string"},
"precision": {"type": "integer", "default": 0},
"inverted": {"type": "boolean", "default": false},
"thumbSize": {"type": "number"},
"restrictToMarks": {"type": "boolean", "default": false}
}
},
"Alert": {
"description": "Alert component for feedback messages",
"props": {
"children": {"type": "any"},
"title": {"type": "any"},
"color": {"type": "string", "default": "blue"},
"variant": {"type": "string", "enum": ["filled", "light", "outline", "default", "transparent", "white"], "default": "light"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"icon": {"type": "any"},
"withCloseButton": {"type": "boolean", "default": false},
"closeButtonLabel": {"type": "string"},
"autoContrast": {"type": "boolean", "default": false}
}
},
"Loader": {
"description": "Loading indicator",
"props": {
"color": {"type": "string", "default": "blue"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"type": {"type": "string", "enum": ["oval", "bars", "dots"], "default": "oval"}
}
},
"Progress": {
"description": "Progress bar",
"props": {
"value": {"type": "number", "required": true},
"color": {"type": "string", "default": "blue"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"striped": {"type": "boolean", "default": false},
"animated": {"type": "boolean", "default": false},
"autoContrast": {"type": "boolean", "default": false},
"transitionDuration": {"type": "number", "default": 100}
}
},
"Modal": {
"description": "Modal dialog overlay",
"props": {
"children": {"type": "any"},
"opened": {"type": "boolean", "required": true},
"title": {"type": "any"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl", "auto"], "default": "md"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"centered": {"type": "boolean", "default": false},
"fullScreen": {"type": "boolean", "default": false},
"withCloseButton": {"type": "boolean", "default": true},
"closeOnClickOutside": {"type": "boolean", "default": true},
"closeOnEscape": {"type": "boolean", "default": true},
"overlayProps": {"type": "object"},
"padding": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"transitionProps": {"type": "object"},
"zIndex": {"type": "number", "default": 200},
"trapFocus": {"type": "boolean", "default": true},
"returnFocus": {"type": "boolean", "default": true},
"lockScroll": {"type": "boolean", "default": true}
}
},
"Drawer": {
"description": "Sliding panel drawer",
"props": {
"children": {"type": "any"},
"opened": {"type": "boolean", "required": true},
"title": {"type": "any"},
"position": {"type": "string", "enum": ["left", "right", "top", "bottom"], "default": "left"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"]},
"withCloseButton": {"type": "boolean", "default": true},
"closeOnClickOutside": {"type": "boolean", "default": true},
"closeOnEscape": {"type": "boolean", "default": true},
"overlayProps": {"type": "object"},
"padding": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"zIndex": {"type": "number", "default": 200},
"offset": {"type": "number", "default": 0},
"trapFocus": {"type": "boolean", "default": true},
"returnFocus": {"type": "boolean", "default": true},
"lockScroll": {"type": "boolean", "default": true}
}
},
"Tooltip": {
"description": "Tooltip on hover",
"props": {
"children": {"type": "any", "required": true},
"label": {"type": "any", "required": true},
"position": {"type": "string", "enum": ["top", "right", "bottom", "left", "top-start", "top-end", "right-start", "right-end", "bottom-start", "bottom-end", "left-start", "left-end"], "default": "top"},
"color": {"type": "string"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"withArrow": {"type": "boolean", "default": false},
"arrowSize": {"type": "number", "default": 4},
"arrowOffset": {"type": "number", "default": 5},
"offset": {"type": "number", "default": 5},
"multiline": {"type": "boolean", "default": false},
"disabled": {"type": "boolean", "default": false},
"openDelay": {"type": "number", "default": 0},
"closeDelay": {"type": "number", "default": 0},
"transitionProps": {"type": "object"},
"zIndex": {"type": "number", "default": 300}
}
},
"Text": {
"description": "Text component with styling",
"props": {
"children": {"type": "any"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"]},
"c": {"type": "string", "description": "Color"},
"fw": {"type": "number", "description": "Font weight"},
"fs": {"type": "string", "enum": ["normal", "italic"], "description": "Font style"},
"td": {"type": "string", "enum": ["none", "underline", "line-through"], "description": "Text decoration"},
"tt": {"type": "string", "enum": ["none", "capitalize", "uppercase", "lowercase"], "description": "Text transform"},
"ta": {"type": "string", "enum": ["left", "center", "right", "justify"], "description": "Text align"},
"lineClamp": {"type": "integer"},
"truncate": {"type": "boolean", "default": false},
"inherit": {"type": "boolean", "default": false},
"gradient": {"type": "object"},
"span": {"type": "boolean", "default": false},
"lh": {"type": "string", "description": "Line height"}
}
},
"Title": {
"description": "Heading component",
"props": {
"children": {"type": "any"},
"order": {"type": "integer", "enum": [1, 2, 3, 4, 5, 6], "default": 1},
"size": {"type": "string"},
"c": {"type": "string", "description": "Color"},
"ta": {"type": "string", "enum": ["left", "center", "right", "justify"]},
"td": {"type": "string", "enum": ["none", "underline", "line-through"]},
"tt": {"type": "string", "enum": ["none", "capitalize", "uppercase", "lowercase"]},
"lineClamp": {"type": "integer"},
"truncate": {"type": "boolean", "default": false}
}
},
"Stack": {
"description": "Vertical stack layout",
"props": {
"children": {"type": "any"},
"gap": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"align": {"type": "string", "enum": ["stretch", "center", "flex-start", "flex-end"], "default": "stretch"},
"justify": {"type": "string", "enum": ["flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly"], "default": "flex-start"}
}
},
"Group": {
"description": "Horizontal group layout",
"props": {
"children": {"type": "any"},
"gap": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"align": {"type": "string", "enum": ["stretch", "center", "flex-start", "flex-end"], "default": "center"},
"justify": {"type": "string", "enum": ["flex-start", "flex-end", "center", "space-between", "space-around"], "default": "flex-start"},
"grow": {"type": "boolean", "default": false},
"wrap": {"type": "string", "enum": ["wrap", "nowrap", "wrap-reverse"], "default": "wrap"},
"preventGrowOverflow": {"type": "boolean", "default": true}
}
},
"Flex": {
"description": "Flexbox container",
"props": {
"children": {"type": "any"},
"gap": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"]},
"rowGap": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"]},
"columnGap": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"]},
"align": {"type": "string", "enum": ["stretch", "center", "flex-start", "flex-end", "baseline"]},
"justify": {"type": "string", "enum": ["flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly"]},
"wrap": {"type": "string", "enum": ["wrap", "nowrap", "wrap-reverse"], "default": "nowrap"},
"direction": {"type": "string", "enum": ["row", "column", "row-reverse", "column-reverse"], "default": "row"}
}
},
"Grid": {
"description": "Grid layout component",
"props": {
"children": {"type": "any"},
"columns": {"type": "integer", "default": 12},
"gutter": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"grow": {"type": "boolean", "default": false},
"justify": {"type": "string", "enum": ["flex-start", "flex-end", "center", "space-between", "space-around"], "default": "flex-start"},
"align": {"type": "string", "enum": ["stretch", "center", "flex-start", "flex-end"], "default": "stretch"},
"overflow": {"type": "string", "enum": ["visible", "hidden"], "default": "visible"}
}
},
"SimpleGrid": {
"description": "Simple grid with equal columns",
"props": {
"children": {"type": "any"},
"cols": {"type": "integer", "default": 1},
"spacing": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"verticalSpacing": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"]}
}
},
"Container": {
"description": "Centered container with max-width",
"props": {
"children": {"type": "any"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"fluid": {"type": "boolean", "default": false}
}
},
"Paper": {
"description": "Paper surface component",
"props": {
"children": {"type": "any"},
"shadow": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"]},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"p": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "description": "Padding"},
"withBorder": {"type": "boolean", "default": false}
}
},
"Card": {
"description": "Card container",
"props": {
"children": {"type": "any"},
"shadow": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"padding": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"withBorder": {"type": "boolean", "default": false}
}
},
"Tabs": {
"description": "Tabbed interface",
"props": {
"children": {"type": "any"},
"value": {"type": "string"},
"defaultValue": {"type": "string"},
"orientation": {"type": "string", "enum": ["horizontal", "vertical"], "default": "horizontal"},
"variant": {"type": "string", "enum": ["default", "outline", "pills"], "default": "default"},
"color": {"type": "string", "default": "blue"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"placement": {"type": "string", "enum": ["left", "right"], "default": "left"},
"grow": {"type": "boolean", "default": false},
"inverted": {"type": "boolean", "default": false},
"keepMounted": {"type": "boolean", "default": true},
"activateTabWithKeyboard": {"type": "boolean", "default": true},
"allowTabDeactivation": {"type": "boolean", "default": false},
"autoContrast": {"type": "boolean", "default": false}
}
},
"Accordion": {
"description": "Collapsible content panels",
"props": {
"children": {"type": "any"},
"value": {"type": "any"},
"defaultValue": {"type": "any"},
"multiple": {"type": "boolean", "default": false},
"variant": {"type": "string", "enum": ["default", "contained", "filled", "separated"], "default": "default"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"chevronPosition": {"type": "string", "enum": ["left", "right"], "default": "right"},
"disableChevronRotation": {"type": "boolean", "default": false},
"transitionDuration": {"type": "number", "default": 200},
"chevronSize": {"type": "any"},
"order": {"type": "integer", "enum": [2, 3, 4, 5, 6]}
}
},
"Badge": {
"description": "Badge for status or labels",
"props": {
"children": {"type": "any"},
"color": {"type": "string", "default": "blue"},
"variant": {"type": "string", "enum": ["filled", "light", "outline", "dot", "gradient", "default", "transparent", "white"], "default": "filled"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "xl"},
"fullWidth": {"type": "boolean", "default": false},
"leftSection": {"type": "any"},
"rightSection": {"type": "any"},
"autoContrast": {"type": "boolean", "default": false},
"circle": {"type": "boolean", "default": false}
}
},
"Avatar": {
"description": "User avatar image",
"props": {
"src": {"type": "string"},
"alt": {"type": "string"},
"children": {"type": "any", "description": "Fallback content"},
"color": {"type": "string", "default": "gray"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "xl"},
"variant": {"type": "string", "enum": ["filled", "light", "outline", "gradient", "default", "transparent", "white"], "default": "filled"},
"autoContrast": {"type": "boolean", "default": false}
}
},
"Image": {
"description": "Image with fallback",
"props": {
"src": {"type": "string"},
"alt": {"type": "string"},
"w": {"type": "any", "description": "Width"},
"h": {"type": "any", "description": "Height"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"]},
"fit": {"type": "string", "enum": ["contain", "cover", "fill", "none", "scale-down"], "default": "cover"},
"fallbackSrc": {"type": "string"}
}
},
"Table": {
"description": "Data table component",
"props": {
"children": {"type": "any"},
"data": {"type": "object", "description": "Table data object with head, body, foot"},
"striped": {"type": "boolean", "default": false},
"highlightOnHover": {"type": "boolean", "default": false},
"withTableBorder": {"type": "boolean", "default": false},
"withColumnBorders": {"type": "boolean", "default": false},
"withRowBorders": {"type": "boolean", "default": true},
"verticalSpacing": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "xs"},
"horizontalSpacing": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "xs"},
"captionSide": {"type": "string", "enum": ["top", "bottom"], "default": "bottom"},
"stickyHeader": {"type": "boolean", "default": false},
"stickyHeaderOffset": {"type": "number", "default": 0}
}
},
"AreaChart": {
"description": "Area chart for time series data",
"props": {
"data": {"type": "array", "required": true},
"dataKey": {"type": "string", "required": true, "description": "X-axis data key"},
"series": {"type": "array", "required": true, "description": "Array of {name, color} objects"},
"h": {"type": "any", "description": "Chart height"},
"w": {"type": "any", "description": "Chart width"},
"curveType": {"type": "string", "enum": ["bump", "linear", "natural", "monotone", "step", "stepBefore", "stepAfter"], "default": "monotone"},
"connectNulls": {"type": "boolean", "default": true},
"withDots": {"type": "boolean", "default": true},
"withGradient": {"type": "boolean", "default": true},
"withLegend": {"type": "boolean", "default": false},
"withTooltip": {"type": "boolean", "default": true},
"withXAxis": {"type": "boolean", "default": true},
"withYAxis": {"type": "boolean", "default": true},
"gridAxis": {"type": "string", "enum": ["x", "y", "xy", "none"], "default": "x"},
"tickLine": {"type": "string", "enum": ["x", "y", "xy", "none"], "default": "y"},
"strokeDasharray": {"type": "string"},
"fillOpacity": {"type": "number", "default": 0.2},
"splitColors": {"type": "array"},
"areaChartProps": {"type": "object"},
"type": {"type": "string", "enum": ["default", "stacked", "percent", "split"], "default": "default"}
}
},
"BarChart": {
"description": "Bar chart for categorical data",
"props": {
"data": {"type": "array", "required": true},
"dataKey": {"type": "string", "required": true},
"series": {"type": "array", "required": true},
"h": {"type": "any"},
"w": {"type": "any"},
"orientation": {"type": "string", "enum": ["horizontal", "vertical"], "default": "vertical"},
"withLegend": {"type": "boolean", "default": false},
"withTooltip": {"type": "boolean", "default": true},
"withXAxis": {"type": "boolean", "default": true},
"withYAxis": {"type": "boolean", "default": true},
"gridAxis": {"type": "string", "enum": ["x", "y", "xy", "none"], "default": "x"},
"tickLine": {"type": "string", "enum": ["x", "y", "xy", "none"], "default": "y"},
"barProps": {"type": "object"},
"type": {"type": "string", "enum": ["default", "stacked", "percent", "waterfall"], "default": "default"}
}
},
"LineChart": {
"description": "Line chart for trends",
"props": {
"data": {"type": "array", "required": true},
"dataKey": {"type": "string", "required": true},
"series": {"type": "array", "required": true},
"h": {"type": "any"},
"w": {"type": "any"},
"curveType": {"type": "string", "enum": ["bump", "linear", "natural", "monotone", "step", "stepBefore", "stepAfter"], "default": "monotone"},
"connectNulls": {"type": "boolean", "default": true},
"withDots": {"type": "boolean", "default": true},
"withLegend": {"type": "boolean", "default": false},
"withTooltip": {"type": "boolean", "default": true},
"withXAxis": {"type": "boolean", "default": true},
"withYAxis": {"type": "boolean", "default": true},
"gridAxis": {"type": "string", "enum": ["x", "y", "xy", "none"], "default": "x"},
"strokeWidth": {"type": "number", "default": 2}
}
},
"PieChart": {
"description": "Pie chart for proportions",
"props": {
"data": {"type": "array", "required": true, "description": "Array of {name, value, color} objects"},
"h": {"type": "any"},
"w": {"type": "any"},
"withLabels": {"type": "boolean", "default": false},
"withLabelsLine": {"type": "boolean", "default": true},
"withTooltip": {"type": "boolean", "default": true},
"labelsPosition": {"type": "string", "enum": ["inside", "outside"], "default": "outside"},
"labelsType": {"type": "string", "enum": ["value", "percent"], "default": "value"},
"strokeWidth": {"type": "number", "default": 1},
"strokeColor": {"type": "string"},
"startAngle": {"type": "number", "default": 0},
"endAngle": {"type": "number", "default": 360}
}
},
"DonutChart": {
"description": "Donut chart (pie with hole)",
"props": {
"data": {"type": "array", "required": true},
"h": {"type": "any"},
"w": {"type": "any"},
"withLabels": {"type": "boolean", "default": false},
"withLabelsLine": {"type": "boolean", "default": true},
"withTooltip": {"type": "boolean", "default": true},
"thickness": {"type": "number", "default": 20},
"chartLabel": {"type": "any"},
"strokeWidth": {"type": "number", "default": 1},
"strokeColor": {"type": "string"},
"startAngle": {"type": "number", "default": 0},
"endAngle": {"type": "number", "default": 360},
"paddingAngle": {"type": "number", "default": 0}
}
},
"DatePicker": {
"description": "Date picker calendar",
"props": {
"value": {"type": "string"},
"type": {"type": "string", "enum": ["default", "range", "multiple"], "default": "default"},
"defaultValue": {"type": "any"},
"allowDeselect": {"type": "boolean", "default": false},
"allowSingleDateInRange": {"type": "boolean", "default": false},
"numberOfColumns": {"type": "integer", "default": 1},
"columnsToScroll": {"type": "integer", "default": 1},
"ariaLabels": {"type": "object"},
"hideOutsideDates": {"type": "boolean", "default": false},
"hideWeekdays": {"type": "boolean", "default": false},
"weekendDays": {"type": "array", "default": [0, 6]},
"renderDay": {"type": "any"},
"minDate": {"type": "string"},
"maxDate": {"type": "string"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"}
}
},
"DatePickerInput": {
"description": "Date picker input field",
"props": {
"value": {"type": "string"},
"label": {"type": "any"},
"description": {"type": "any"},
"error": {"type": "any"},
"placeholder": {"type": "string"},
"clearable": {"type": "boolean", "default": false},
"type": {"type": "string", "enum": ["default", "range", "multiple"], "default": "default"},
"valueFormat": {"type": "string", "default": "MMMM D, YYYY"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"disabled": {"type": "boolean", "default": false},
"required": {"type": "boolean", "default": false},
"minDate": {"type": "string"},
"maxDate": {"type": "string"},
"popoverProps": {"type": "object"},
"dropdownType": {"type": "string", "enum": ["popover", "modal"], "default": "popover"}
}
},
"DatesProvider": {
"description": "Provider for date localization settings",
"props": {
"children": {"type": "any", "required": true},
"settings": {"type": "object", "description": "Locale and formatting settings"}
}
}
}
}

View File

@@ -1,16 +0,0 @@
# MCP SDK
mcp>=0.9.0
# Visualization
plotly>=5.18.0
dash>=2.14.0
dash-mantine-components>=2.0.0
kaleido>=0.2.1 # For chart export (PNG, SVG, PDF)
# Utilities
python-dotenv>=1.0.0
pydantic>=2.5.0
# Testing
pytest>=7.4.3
pytest-asyncio>=0.23.0

View File

@@ -1,21 +0,0 @@
#!/bin/bash
# Capture original working directory before any cd operations
# This should be the user's project directory when launched by Claude Code
export CLAUDE_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CACHE_VENV="$HOME/.cache/claude-mcp-venvs/leo-claude-mktplace/viz-platform/.venv"
LOCAL_VENV="$SCRIPT_DIR/.venv"
if [[ -f "$CACHE_VENV/bin/python" ]]; then
PYTHON="$CACHE_VENV/bin/python"
elif [[ -f "$LOCAL_VENV/bin/python" ]]; then
PYTHON="$LOCAL_VENV/bin/python"
else
echo "ERROR: No venv found. Run: ./scripts/setup-venvs.sh" >&2
exit 1
fi
cd "$SCRIPT_DIR"
export PYTHONPATH="$SCRIPT_DIR"
exec "$PYTHON" -m mcp_server.server "$@"

View File

@@ -1,262 +0,0 @@
#!/usr/bin/env python3
"""
Generate DMC Component Registry from installed dash-mantine-components package.
This script introspects the installed DMC package and generates a JSON registry
file containing component definitions, props, types, and defaults.
Usage:
python generate-dmc-registry.py [--output registry/dmc_X_Y.json]
Requirements:
- dash-mantine-components must be installed
- Run from the mcp-servers/viz-platform directory
"""
import argparse
import inspect
import json
import sys
from datetime import date
from pathlib import Path
from typing import Any, Dict, List, Optional, get_type_hints
def get_dmc_version() -> Optional[str]:
"""Get installed DMC version."""
try:
from importlib.metadata import version
return version('dash-mantine-components')
except Exception:
return None
def get_component_categories() -> Dict[str, List[str]]:
"""Define component categories."""
return {
"buttons": ["Button", "ActionIcon", "CopyButton", "FileButton", "UnstyledButton"],
"inputs": [
"TextInput", "PasswordInput", "NumberInput", "Textarea",
"Select", "MultiSelect", "Checkbox", "Switch", "Radio",
"Slider", "RangeSlider", "ColorInput", "ColorPicker",
"DateInput", "DatePicker", "TimeInput"
],
"navigation": ["Anchor", "Breadcrumbs", "Burger", "NavLink", "Pagination", "Stepper", "Tabs"],
"feedback": ["Alert", "Loader", "Notification", "Progress", "RingProgress", "Skeleton"],
"overlays": ["Dialog", "Drawer", "HoverCard", "Menu", "Modal", "Popover", "Tooltip"],
"typography": ["Blockquote", "Code", "Highlight", "Mark", "Text", "Title"],
"layout": [
"AppShell", "AspectRatio", "Center", "Container", "Flex",
"Grid", "Group", "Paper", "SimpleGrid", "Space", "Stack"
],
"data": [
"Accordion", "Avatar", "Badge", "Card", "Image",
"Indicator", "Kbd", "Spoiler", "Table", "ThemeIcon", "Timeline"
]
}
def extract_prop_type(prop_info: Dict[str, Any]) -> Dict[str, Any]:
"""Extract prop type information from Dash component prop."""
result = {"type": "any"}
if 'type' not in prop_info:
return result
prop_type = prop_info['type']
if isinstance(prop_type, dict):
type_name = prop_type.get('name', 'any')
# Map Dash types to JSON schema types
type_mapping = {
'string': 'string',
'number': 'number',
'bool': 'boolean',
'boolean': 'boolean',
'array': 'array',
'object': 'object',
'node': 'any',
'element': 'any',
'any': 'any',
'func': 'any',
}
result['type'] = type_mapping.get(type_name, 'any')
# Handle enums
if type_name == 'enum' and 'value' in prop_type:
values = prop_type['value']
if isinstance(values, list):
enum_values = []
for v in values:
if isinstance(v, dict) and 'value' in v:
# Remove quotes from string values
val = v['value'].strip("'\"")
enum_values.append(val)
elif isinstance(v, str):
enum_values.append(v.strip("'\""))
if enum_values:
result['enum'] = enum_values
result['type'] = 'string'
# Handle union types
elif type_name == 'union' and 'value' in prop_type:
# For unions, just mark as any for simplicity
result['type'] = 'any'
elif isinstance(prop_type, str):
result['type'] = prop_type
return result
def extract_component_props(component_class) -> Dict[str, Any]:
"""Extract props from a Dash component class."""
props = {}
# Try to get _prop_names or similar
if hasattr(component_class, '_prop_names'):
prop_names = component_class._prop_names
else:
prop_names = []
# Try to get _type attribute for prop definitions
if hasattr(component_class, '_type'):
prop_types = getattr(component_class, '_type', {})
else:
prop_types = {}
# Get default values
if hasattr(component_class, '_default_props'):
defaults = component_class._default_props
else:
defaults = {}
# Try to extract from _prop_descriptions
if hasattr(component_class, '_prop_descriptions'):
descriptions = component_class._prop_descriptions
else:
descriptions = {}
for prop_name in prop_names:
if prop_name.startswith('_'):
continue
prop_info = {}
# Get type info if available
if prop_name in prop_types:
prop_info = extract_prop_type({'type': prop_types[prop_name]})
else:
prop_info = {'type': 'any'}
# Add default if exists
if prop_name in defaults:
prop_info['default'] = defaults[prop_name]
# Add description if exists
if prop_name in descriptions:
prop_info['description'] = descriptions[prop_name]
props[prop_name] = prop_info
return props
def generate_registry() -> Dict[str, Any]:
"""Generate the component registry from installed DMC."""
try:
import dash_mantine_components as dmc
except ImportError:
print("ERROR: dash-mantine-components not installed")
print("Install with: pip install dash-mantine-components")
sys.exit(1)
version = get_dmc_version()
categories = get_component_categories()
registry = {
"version": version,
"generated": date.today().isoformat(),
"categories": categories,
"components": {}
}
# Get all components from categories
all_components = set()
for comp_list in categories.values():
all_components.update(comp_list)
# Extract props for each component
for comp_name in sorted(all_components):
if hasattr(dmc, comp_name):
comp_class = getattr(dmc, comp_name)
try:
props = extract_component_props(comp_class)
if props:
registry["components"][comp_name] = {
"description": comp_class.__doc__ or f"{comp_name} component",
"props": props
}
print(f" Extracted: {comp_name} ({len(props)} props)")
except Exception as e:
print(f" Warning: Failed to extract {comp_name}: {e}")
else:
print(f" Warning: Component not found: {comp_name}")
return registry
def main():
parser = argparse.ArgumentParser(
description="Generate DMC component registry from installed package"
)
parser.add_argument(
'--output', '-o',
type=str,
help='Output file path (default: auto-generated based on version)'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Print to stdout instead of writing file'
)
args = parser.parse_args()
print("Generating DMC Component Registry...")
print("=" * 50)
registry = generate_registry()
print("=" * 50)
print(f"Generated registry for DMC {registry['version']}")
print(f"Total components: {len(registry['components'])}")
if args.dry_run:
print(json.dumps(registry, indent=2))
return
# Determine output path
if args.output:
output_path = Path(args.output)
else:
version = registry['version']
if version:
major_minor = '_'.join(version.split('.')[:2])
output_path = Path(__file__).parent.parent / 'registry' / f'dmc_{major_minor}.json'
else:
output_path = Path(__file__).parent.parent / 'registry' / 'dmc_unknown.json'
# Create directory if needed
output_path.parent.mkdir(parents=True, exist_ok=True)
# Write registry
with open(output_path, 'w') as f:
json.dump(registry, indent=2, fp=f)
print(f"Registry written to: {output_path}")
if __name__ == "__main__":
main()

View File

@@ -1 +0,0 @@
"""viz-platform MCP Server tests."""

View File

@@ -1,195 +0,0 @@
"""
Tests for accessibility validation tools.
"""
import pytest
from mcp_server.accessibility_tools import AccessibilityTools
@pytest.fixture
def tools():
"""Create AccessibilityTools instance."""
return AccessibilityTools()
class TestHexToRgb:
"""Tests for _hex_to_rgb method."""
def test_hex_to_rgb_6_digit(self, tools):
"""Test 6-digit hex conversion."""
assert tools._hex_to_rgb("#FF0000") == (255, 0, 0)
assert tools._hex_to_rgb("#00FF00") == (0, 255, 0)
assert tools._hex_to_rgb("#0000FF") == (0, 0, 255)
def test_hex_to_rgb_3_digit(self, tools):
"""Test 3-digit hex conversion."""
assert tools._hex_to_rgb("#F00") == (255, 0, 0)
assert tools._hex_to_rgb("#0F0") == (0, 255, 0)
assert tools._hex_to_rgb("#00F") == (0, 0, 255)
def test_hex_to_rgb_lowercase(self, tools):
"""Test lowercase hex conversion."""
assert tools._hex_to_rgb("#ff0000") == (255, 0, 0)
class TestContrastRatio:
"""Tests for _get_contrast_ratio method."""
def test_black_white_contrast(self, tools):
"""Test black on white has maximum contrast."""
ratio = tools._get_contrast_ratio("#000000", "#FFFFFF")
assert ratio == pytest.approx(21.0, rel=0.01)
def test_same_color_contrast(self, tools):
"""Test same color has minimum contrast."""
ratio = tools._get_contrast_ratio("#FF0000", "#FF0000")
assert ratio == pytest.approx(1.0, rel=0.01)
def test_symmetric_contrast(self, tools):
"""Test contrast ratio is symmetric."""
ratio1 = tools._get_contrast_ratio("#228be6", "#FFFFFF")
ratio2 = tools._get_contrast_ratio("#FFFFFF", "#228be6")
assert ratio1 == pytest.approx(ratio2, rel=0.01)
class TestColorBlindnessSimulation:
"""Tests for _simulate_color_blindness method."""
def test_deuteranopia_simulation(self, tools):
"""Test deuteranopia (green-blind) simulation."""
# Red and green should appear more similar
original_red = "#FF0000"
original_green = "#00FF00"
simulated_red = tools._simulate_color_blindness(original_red, "deuteranopia")
simulated_green = tools._simulate_color_blindness(original_green, "deuteranopia")
# They should be different from originals
assert simulated_red != original_red or simulated_green != original_green
def test_protanopia_simulation(self, tools):
"""Test protanopia (red-blind) simulation."""
simulated = tools._simulate_color_blindness("#FF0000", "protanopia")
# Should return a modified color
assert simulated.startswith("#")
assert len(simulated) == 7
def test_tritanopia_simulation(self, tools):
"""Test tritanopia (blue-blind) simulation."""
simulated = tools._simulate_color_blindness("#0000FF", "tritanopia")
# Should return a modified color
assert simulated.startswith("#")
assert len(simulated) == 7
def test_unknown_deficiency_returns_original(self, tools):
"""Test unknown deficiency type returns original color."""
color = "#FF0000"
simulated = tools._simulate_color_blindness(color, "unknown")
assert simulated == color
class TestAccessibilityValidateColors:
"""Tests for accessibility_validate_colors method."""
@pytest.mark.asyncio
async def test_validate_single_color(self, tools):
"""Test validating a single color."""
result = await tools.accessibility_validate_colors(["#228be6"])
assert "colors_checked" in result
assert "overall_score" in result
assert "issues" in result
assert "safe_palettes" in result
@pytest.mark.asyncio
async def test_validate_problematic_colors(self, tools):
"""Test similar colors trigger warnings."""
# Use colors that are very close in hue, which should be harder to distinguish
result = await tools.accessibility_validate_colors(["#FF5555", "#FF6666"])
# Similar colors should trigger distinguishability warnings
assert "issues" in result
# The validation should at least run without errors
assert "colors_checked" in result
assert len(result["colors_checked"]) == 2
@pytest.mark.asyncio
async def test_validate_contrast_issue(self, tools):
"""Test low contrast colors trigger contrast warnings."""
# Yellow on white has poor contrast
result = await tools.accessibility_validate_colors(["#FFFF00"])
# Check for contrast issues (yellow may have issues with both black and white)
assert "issues" in result
@pytest.mark.asyncio
async def test_validate_with_specific_types(self, tools):
"""Test validating for specific color blindness types."""
result = await tools.accessibility_validate_colors(
["#FF0000", "#00FF00"],
check_types=["deuteranopia"]
)
assert "simulations" in result
assert "deuteranopia" in result["simulations"]
assert "protanopia" not in result["simulations"]
@pytest.mark.asyncio
async def test_overall_score(self, tools):
"""Test overall score is calculated."""
result = await tools.accessibility_validate_colors(["#228be6", "#ffffff"])
assert result["overall_score"] in ["A", "B", "C", "D"]
@pytest.mark.asyncio
async def test_recommendations_generated(self, tools):
"""Test recommendations are generated for issues."""
result = await tools.accessibility_validate_colors(["#FF0000", "#00FF00"])
assert "recommendations" in result
assert len(result["recommendations"]) > 0
class TestAccessibilitySuggestAlternative:
"""Tests for accessibility_suggest_alternative method."""
@pytest.mark.asyncio
async def test_suggest_alternative_deuteranopia(self, tools):
"""Test suggesting alternatives for deuteranopia."""
result = await tools.accessibility_suggest_alternative("#FF0000", "deuteranopia")
assert "original_color" in result
assert result["deficiency_type"] == "deuteranopia"
assert "suggestions" in result
assert len(result["suggestions"]) > 0
@pytest.mark.asyncio
async def test_suggest_alternative_tritanopia(self, tools):
"""Test suggesting alternatives for tritanopia."""
result = await tools.accessibility_suggest_alternative("#0000FF", "tritanopia")
assert "suggestions" in result
assert len(result["suggestions"]) > 0
@pytest.mark.asyncio
async def test_suggestions_include_safe_palettes(self, tools):
"""Test suggestions include colors from safe palettes."""
result = await tools.accessibility_suggest_alternative("#FF0000", "deuteranopia")
palette_suggestions = [
s for s in result["suggestions"]
if "palette" in s
]
assert len(palette_suggestions) > 0
class TestSafePalettes:
"""Tests for safe palette constants."""
def test_safe_palettes_exist(self, tools):
"""Test that safe palettes are defined."""
from mcp_server.accessibility_tools import SAFE_PALETTES
assert "categorical" in SAFE_PALETTES
assert "ibm" in SAFE_PALETTES
assert "okabe_ito" in SAFE_PALETTES
assert "tableau_colorblind" in SAFE_PALETTES
def test_safe_palettes_have_colors(self, tools):
"""Test that safe palettes have color lists."""
from mcp_server.accessibility_tools import SAFE_PALETTES
for palette_name, palette in SAFE_PALETTES.items():
assert "colors" in palette
assert len(palette["colors"]) > 0
# All colors should be valid hex
for color in palette["colors"]:
assert color.startswith("#")

View File

@@ -1,271 +0,0 @@
"""
Unit tests for chart creation tools.
"""
import pytest
from unittest.mock import MagicMock, patch
@pytest.fixture
def chart_tools():
"""Create ChartTools instance"""
from mcp_server.chart_tools import ChartTools
return ChartTools()
@pytest.fixture
def chart_tools_with_theme():
"""Create ChartTools instance with a theme"""
from mcp_server.chart_tools import ChartTools
tools = ChartTools()
tools.set_theme({
"colors": {
"primary": "#ff0000",
"secondary": "#00ff00",
"success": "#0000ff"
}
})
return tools
def test_chart_tools_init():
"""Test chart tools initialization"""
from mcp_server.chart_tools import ChartTools
tools = ChartTools()
assert tools.theme_store is None
assert tools._active_theme is None
def test_set_theme(chart_tools):
"""Test setting active theme"""
theme = {"colors": {"primary": "#123456"}}
chart_tools.set_theme(theme)
assert chart_tools._active_theme == theme
def test_get_color_palette_default(chart_tools):
"""Test getting default color palette"""
from mcp_server.chart_tools import DEFAULT_COLORS
palette = chart_tools._get_color_palette()
assert palette == DEFAULT_COLORS
def test_get_color_palette_with_theme(chart_tools_with_theme):
"""Test getting color palette from theme"""
palette = chart_tools_with_theme._get_color_palette()
# Should start with theme colors
assert palette[0] == "#ff0000"
assert palette[1] == "#00ff00"
assert palette[2] == "#0000ff"
def test_resolve_color_from_theme(chart_tools_with_theme):
"""Test resolving color token from theme"""
color = chart_tools_with_theme._resolve_color("primary")
assert color == "#ff0000"
def test_resolve_color_hex(chart_tools):
"""Test resolving hex color"""
color = chart_tools._resolve_color("#abcdef")
assert color == "#abcdef"
def test_resolve_color_rgb(chart_tools):
"""Test resolving rgb color"""
color = chart_tools._resolve_color("rgb(255, 0, 0)")
assert color == "rgb(255, 0, 0)"
def test_resolve_color_named(chart_tools):
"""Test resolving named color"""
color = chart_tools._resolve_color("blue")
assert color == "#228be6" # DEFAULT_COLORS[0]
def test_resolve_color_none(chart_tools):
"""Test resolving None color defaults to first palette color"""
from mcp_server.chart_tools import DEFAULT_COLORS
color = chart_tools._resolve_color(None)
assert color == DEFAULT_COLORS[0]
@pytest.mark.asyncio
async def test_chart_create_line(chart_tools):
"""Test creating a line chart"""
data = {
"x": [1, 2, 3, 4, 5],
"y": [10, 20, 15, 25, 30]
}
result = await chart_tools.chart_create("line", data)
assert "figure" in result
assert result["chart_type"] == "line"
assert "error" not in result or result["error"] is None
@pytest.mark.asyncio
async def test_chart_create_bar(chart_tools):
"""Test creating a bar chart"""
data = {
"x": ["A", "B", "C"],
"y": [10, 20, 15]
}
result = await chart_tools.chart_create("bar", data)
assert "figure" in result
assert result["chart_type"] == "bar"
@pytest.mark.asyncio
async def test_chart_create_scatter(chart_tools):
"""Test creating a scatter chart"""
data = {
"x": [1, 2, 3, 4, 5],
"y": [10, 20, 15, 25, 30]
}
result = await chart_tools.chart_create("scatter", data)
assert "figure" in result
assert result["chart_type"] == "scatter"
@pytest.mark.asyncio
async def test_chart_create_pie(chart_tools):
"""Test creating a pie chart"""
data = {
"labels": ["A", "B", "C"],
"values": [30, 50, 20]
}
result = await chart_tools.chart_create("pie", data)
assert "figure" in result
assert result["chart_type"] == "pie"
@pytest.mark.asyncio
async def test_chart_create_histogram(chart_tools):
"""Test creating a histogram"""
data = {
"x": [1, 1, 2, 2, 2, 3, 3, 4, 5, 5, 5, 5]
}
result = await chart_tools.chart_create("histogram", data)
assert "figure" in result
assert result["chart_type"] == "histogram"
@pytest.mark.asyncio
async def test_chart_create_area(chart_tools):
"""Test creating an area chart"""
data = {
"x": [1, 2, 3, 4, 5],
"y": [10, 20, 15, 25, 30]
}
result = await chart_tools.chart_create("area", data)
assert "figure" in result
assert result["chart_type"] == "area"
@pytest.mark.asyncio
async def test_chart_create_heatmap(chart_tools):
"""Test creating a heatmap"""
data = {
"z": [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
"x": ["A", "B", "C"],
"y": ["X", "Y", "Z"]
}
result = await chart_tools.chart_create("heatmap", data)
assert "figure" in result
assert result["chart_type"] == "heatmap"
@pytest.mark.asyncio
async def test_chart_create_invalid_type(chart_tools):
"""Test creating chart with invalid type"""
data = {"x": [1, 2, 3], "y": [10, 20, 30]}
result = await chart_tools.chart_create("invalid_type", data)
assert "error" in result
assert "invalid" in result["error"].lower()
@pytest.mark.asyncio
async def test_chart_create_with_options(chart_tools):
"""Test creating chart with options"""
data = {
"x": [1, 2, 3],
"y": [10, 20, 30]
}
options = {
"title": "My Chart",
"color": "red"
}
result = await chart_tools.chart_create("line", data, options=options)
assert "figure" in result
# The title should be applied to the figure
@pytest.mark.asyncio
async def test_chart_create_with_theme(chart_tools_with_theme):
"""Test that theme colors are applied to chart"""
data = {
"x": [1, 2, 3],
"y": [10, 20, 30]
}
result = await chart_tools_with_theme.chart_create("line", data)
assert "figure" in result
# Chart should use theme colors
@pytest.mark.asyncio
async def test_chart_configure_interaction(chart_tools):
"""Test configuring chart interaction"""
# Create a simple figure first
data = {"x": [1, 2, 3], "y": [10, 20, 30]}
chart_result = await chart_tools.chart_create("line", data)
figure = chart_result.get("figure", {})
if hasattr(chart_tools, 'chart_configure_interaction'):
result = await chart_tools.chart_configure_interaction(
figure=figure,
interactions={"zoom": True, "pan": True}
)
# Just verify it doesn't crash
assert result is not None
def test_default_colors_defined():
"""Test that DEFAULT_COLORS is properly defined"""
from mcp_server.chart_tools import DEFAULT_COLORS
assert len(DEFAULT_COLORS) == 10
assert all(c.startswith("#") for c in DEFAULT_COLORS)

View File

@@ -1,292 +0,0 @@
"""
Unit tests for DMC component registry.
"""
import pytest
import json
from pathlib import Path
from unittest.mock import patch, MagicMock
@pytest.fixture
def sample_registry_data():
"""Sample registry data for testing"""
return {
"version": "2.5.1",
"categories": {
"buttons": ["Button", "ActionIcon"],
"inputs": ["TextInput", "NumberInput", "Select"]
},
"components": {
"Button": {
"description": "Button component",
"props": {
"variant": {
"type": "string",
"enum": ["filled", "outline", "light"],
"default": "filled"
},
"color": {
"type": "string",
"default": "blue"
},
"size": {
"type": "string",
"enum": ["xs", "sm", "md", "lg", "xl"],
"default": "sm"
},
"disabled": {
"type": "boolean",
"default": False
}
}
},
"TextInput": {
"description": "Text input field",
"props": {
"value": {"type": "string", "default": ""},
"placeholder": {"type": "string"},
"disabled": {"type": "boolean", "default": False},
"required": {"type": "boolean", "default": False}
}
}
}
}
@pytest.fixture
def registry_file(tmp_path, sample_registry_data):
"""Create a temporary registry file"""
registry_dir = tmp_path / "registry"
registry_dir.mkdir()
registry_file = registry_dir / "dmc_2_5.json"
registry_file.write_text(json.dumps(sample_registry_data))
return registry_file
@pytest.fixture
def registry(registry_file):
"""Create a ComponentRegistry with mock registry directory"""
from mcp_server.component_registry import ComponentRegistry
reg = ComponentRegistry(dmc_version="2.5.1")
reg.registry_dir = registry_file.parent
reg.load()
return reg
def test_registry_init():
"""Test registry initialization"""
from mcp_server.component_registry import ComponentRegistry
reg = ComponentRegistry(dmc_version="2.5.1")
assert reg.dmc_version == "2.5.1"
assert reg.components == {}
assert reg.categories == {}
assert reg.loaded_version is None
def test_registry_load_success(registry, sample_registry_data):
"""Test successful registry loading"""
assert registry.is_loaded()
assert registry.loaded_version == "2.5.1"
assert len(registry.components) == 2
assert "Button" in registry.components
assert "TextInput" in registry.components
def test_registry_load_no_file():
"""Test registry loading when no file exists"""
from mcp_server.component_registry import ComponentRegistry
reg = ComponentRegistry(dmc_version="99.99.99")
reg.registry_dir = Path("/nonexistent/path")
result = reg.load()
assert result is False
assert not reg.is_loaded()
def test_get_component(registry):
"""Test getting a component by name"""
button = registry.get_component("Button")
assert button is not None
assert button["description"] == "Button component"
assert "props" in button
def test_get_component_not_found(registry):
"""Test getting a nonexistent component"""
result = registry.get_component("NonexistentComponent")
assert result is None
def test_get_component_props(registry):
"""Test getting component props"""
props = registry.get_component_props("Button")
assert props is not None
assert "variant" in props
assert "color" in props
assert props["variant"]["type"] == "string"
assert props["variant"]["enum"] == ["filled", "outline", "light"]
def test_get_component_props_not_found(registry):
"""Test getting props for nonexistent component"""
props = registry.get_component_props("Nonexistent")
assert props is None
def test_list_components_all(registry):
"""Test listing all components"""
result = registry.list_components()
assert "buttons" in result
assert "inputs" in result
assert "Button" in result["buttons"]
assert "TextInput" in result["inputs"]
def test_list_components_by_category(registry):
"""Test listing components by category"""
result = registry.list_components(category="buttons")
assert len(result) == 1
assert "buttons" in result
assert "Button" in result["buttons"]
def test_list_components_invalid_category(registry):
"""Test listing components with invalid category"""
result = registry.list_components(category="nonexistent")
assert result == {}
def test_get_categories(registry):
"""Test getting available categories"""
categories = registry.get_categories()
assert "buttons" in categories
assert "inputs" in categories
def test_validate_prop_valid_enum(registry):
"""Test validating a valid enum prop"""
result = registry.validate_prop("Button", "variant", "filled")
assert result["valid"] is True
def test_validate_prop_invalid_enum(registry):
"""Test validating an invalid enum prop"""
result = registry.validate_prop("Button", "variant", "invalid_variant")
assert result["valid"] is False
assert "expects one of" in result["error"]
def test_validate_prop_valid_type(registry):
"""Test validating a valid type"""
result = registry.validate_prop("Button", "disabled", True)
assert result["valid"] is True
def test_validate_prop_invalid_type(registry):
"""Test validating an invalid type"""
result = registry.validate_prop("Button", "disabled", "not_a_boolean")
assert result["valid"] is False
assert "expects type" in result["error"]
def test_validate_prop_unknown_component(registry):
"""Test validating prop for unknown component"""
result = registry.validate_prop("Nonexistent", "prop", "value")
assert result["valid"] is False
assert "Unknown component" in result["error"]
def test_validate_prop_unknown_prop(registry):
"""Test validating an unknown prop"""
result = registry.validate_prop("Button", "unknownProp", "value")
assert result["valid"] is False
assert "Unknown prop" in result["error"]
def test_validate_prop_typo_detection(registry):
"""Test typo detection for similar prop names"""
# colour vs color
result = registry.validate_prop("Button", "colour", "blue")
assert result["valid"] is False
# Should suggest 'color'
assert "color" in result.get("error", "").lower()
def test_find_similar_props(registry):
"""Test finding similar prop names"""
available = ["color", "variant", "size", "disabled"]
# Should match despite case difference
similar = registry._find_similar_props("Color", available)
assert similar == "color"
# Should match with slight typo
similar = registry._find_similar_props("colours", ["color", "variant"])
# May or may not match depending on heuristic
def test_load_registry_convenience_function(registry_file):
"""Test the convenience function"""
from mcp_server.component_registry import load_registry, ComponentRegistry
with patch.object(ComponentRegistry, '__init__', return_value=None) as mock_init:
with patch.object(ComponentRegistry, 'load', return_value=True):
mock_init.return_value = None
# Can't easily test this without mocking more - just ensure it doesn't crash
pass
def test_find_registry_file_exact_match(tmp_path):
"""Test finding exact registry file match"""
from mcp_server.component_registry import ComponentRegistry
# Create registry files
registry_dir = tmp_path / "registry"
registry_dir.mkdir()
(registry_dir / "dmc_2_5.json").write_text('{"version": "2.5.0"}')
reg = ComponentRegistry(dmc_version="2.5.1")
reg.registry_dir = registry_dir
result = reg._find_registry_file()
assert result is not None
assert result.name == "dmc_2_5.json"
def test_find_registry_file_fallback(tmp_path):
"""Test fallback to latest registry when no exact match"""
from mcp_server.component_registry import ComponentRegistry
# Create registry files
registry_dir = tmp_path / "registry"
registry_dir.mkdir()
(registry_dir / "dmc_0_14.json").write_text('{"version": "0.14.0"}')
reg = ComponentRegistry(dmc_version="2.5.1") # No exact match
reg.registry_dir = registry_dir
result = reg._find_registry_file()
assert result is not None
assert result.name == "dmc_0_14.json" # Falls back to available

View File

@@ -1,156 +0,0 @@
"""
Unit tests for viz-platform configuration loader.
"""
import pytest
import os
from pathlib import Path
from unittest.mock import patch, MagicMock
@pytest.fixture
def clean_env():
"""Clean environment variables before test"""
env_vars = ['DMC_VERSION', 'CLAUDE_PROJECT_DIR', 'VIZ_DEFAULT_THEME']
saved = {k: os.environ.get(k) for k in env_vars}
for k in env_vars:
if k in os.environ:
del os.environ[k]
yield
# Restore after test
for k, v in saved.items():
if v is not None:
os.environ[k] = v
elif k in os.environ:
del os.environ[k]
@pytest.fixture
def config():
"""Create VizPlatformConfig instance"""
from mcp_server.config import VizPlatformConfig
return VizPlatformConfig()
def test_config_init(config):
"""Test config initialization"""
assert config.dmc_version is None
assert config.theme_dir_user == Path.home() / '.config' / 'claude' / 'themes'
assert config.theme_dir_project is None
assert config.default_theme is None
def test_config_load_returns_dict(config, clean_env):
"""Test config.load() returns expected structure"""
result = config.load()
assert isinstance(result, dict)
assert 'dmc_version' in result
assert 'dmc_available' in result
assert 'theme_dir_user' in result
assert 'theme_dir_project' in result
assert 'default_theme' in result
assert 'project_dir' in result
def test_config_respects_env_dmc_version(config, clean_env):
"""Test that DMC_VERSION env var is respected"""
os.environ['DMC_VERSION'] = '0.14.7'
result = config.load()
assert result['dmc_version'] == '0.14.7'
assert result['dmc_available'] is True
def test_config_respects_default_theme_env(config, clean_env):
"""Test that VIZ_DEFAULT_THEME env var is respected"""
os.environ['VIZ_DEFAULT_THEME'] = 'my-dark-theme'
result = config.load()
assert result['default_theme'] == 'my-dark-theme'
def test_detect_dmc_version_not_installed(config):
"""Test DMC version detection when not installed"""
with patch('importlib.metadata.version', side_effect=ImportError("not installed")):
version = config._detect_dmc_version()
assert version is None
def test_detect_dmc_version_installed(config):
"""Test DMC version detection when installed"""
with patch('importlib.metadata.version', return_value='0.14.7'):
version = config._detect_dmc_version()
assert version == '0.14.7'
def test_find_project_directory_from_env(config, clean_env, tmp_path):
"""Test project directory detection from CLAUDE_PROJECT_DIR"""
os.environ['CLAUDE_PROJECT_DIR'] = str(tmp_path)
result = config._find_project_directory()
assert result == tmp_path
def test_find_project_directory_with_git(config, clean_env, tmp_path):
"""Test project directory detection with .git folder"""
git_dir = tmp_path / '.git'
git_dir.mkdir()
with patch.dict(os.environ, {'PWD': str(tmp_path)}):
result = config._find_project_directory()
assert result == tmp_path
def test_find_project_directory_with_env_file(config, clean_env, tmp_path):
"""Test project directory detection with .env file"""
env_file = tmp_path / '.env'
env_file.touch()
with patch.dict(os.environ, {'PWD': str(tmp_path)}):
result = config._find_project_directory()
assert result == tmp_path
def test_load_config_convenience_function(clean_env):
"""Test the convenience function load_config()"""
from mcp_server.config import load_config
result = load_config()
assert isinstance(result, dict)
assert 'dmc_version' in result
def test_check_dmc_version_not_installed(clean_env):
"""Test check_dmc_version when DMC not installed"""
from mcp_server.config import check_dmc_version
with patch('mcp_server.config.load_config', return_value={'dmc_available': False}):
result = check_dmc_version()
assert result['installed'] is False
assert 'not installed' in result['message'].lower()
def test_check_dmc_version_installed_with_registry(clean_env, tmp_path):
"""Test check_dmc_version when DMC installed with matching registry"""
from mcp_server.config import check_dmc_version
mock_config = {
'dmc_available': True,
'dmc_version': '2.5.1'
}
with patch('mcp_server.config.load_config', return_value=mock_config):
with patch('pathlib.Path.exists', return_value=True):
result = check_dmc_version()
assert result['installed'] is True
assert result['version'] == '2.5.1'

View File

@@ -1,283 +0,0 @@
"""
Unit tests for DMC validation tools.
"""
import pytest
from unittest.mock import MagicMock, patch
@pytest.fixture
def mock_registry():
"""Create a mock component registry"""
registry = MagicMock()
registry.is_loaded.return_value = True
registry.loaded_version = "2.5.1"
registry.categories = {
"buttons": ["Button", "ActionIcon"],
"inputs": ["TextInput", "Select"]
}
registry.list_components.return_value = registry.categories
registry.get_categories.return_value = ["buttons", "inputs"]
# Mock Button component
registry.get_component.side_effect = lambda name: {
"Button": {
"description": "Button component",
"props": {
"variant": {"type": "string", "enum": ["filled", "outline"], "default": "filled"},
"color": {"type": "string", "default": "blue"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg"], "default": "sm"},
"disabled": {"type": "boolean", "default": False, "required": False}
}
},
"TextInput": {
"description": "Text input",
"props": {
"value": {"type": "string", "required": True},
"placeholder": {"type": "string"}
}
}
}.get(name)
registry.get_component_props.side_effect = lambda name: {
"Button": {
"variant": {"type": "string", "enum": ["filled", "outline"], "default": "filled"},
"color": {"type": "string", "default": "blue"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg"], "default": "sm"},
"disabled": {"type": "boolean", "default": False}
},
"TextInput": {
"value": {"type": "string", "required": True},
"placeholder": {"type": "string"}
}
}.get(name)
registry.validate_prop.side_effect = lambda comp, prop, val: (
{"valid": True} if prop in ["variant", "color", "size", "disabled", "value", "placeholder"]
else {"valid": False, "error": f"Unknown prop '{prop}'"}
)
return registry
@pytest.fixture
def dmc_tools(mock_registry):
"""Create DMCTools instance with mock registry"""
from mcp_server.dmc_tools import DMCTools
tools = DMCTools(registry=mock_registry)
tools._initialized = True
return tools
@pytest.fixture
def uninitialized_tools():
"""Create uninitialized DMCTools instance"""
from mcp_server.dmc_tools import DMCTools
return DMCTools()
@pytest.mark.asyncio
async def test_list_components_all(dmc_tools):
"""Test listing all components"""
result = await dmc_tools.list_components()
assert "components" in result
assert "categories" in result
assert "version" in result
assert result["version"] == "2.5.1"
@pytest.mark.asyncio
async def test_list_components_by_category(dmc_tools, mock_registry):
"""Test listing components by category"""
mock_registry.list_components.return_value = {"buttons": ["Button", "ActionIcon"]}
result = await dmc_tools.list_components(category="buttons")
assert "buttons" in result["components"]
mock_registry.list_components.assert_called_with("buttons")
@pytest.mark.asyncio
async def test_list_components_not_initialized(uninitialized_tools):
"""Test listing components when not initialized"""
result = await uninitialized_tools.list_components()
assert "error" in result
assert result["total_count"] == 0
@pytest.mark.asyncio
async def test_get_component_props_success(dmc_tools):
"""Test getting component props"""
result = await dmc_tools.get_component_props("Button")
assert result["component"] == "Button"
assert "props" in result
assert result["prop_count"] > 0
@pytest.mark.asyncio
async def test_get_component_props_not_found(dmc_tools, mock_registry):
"""Test getting props for nonexistent component"""
mock_registry.get_component.return_value = None
result = await dmc_tools.get_component_props("Nonexistent")
assert "error" in result
assert "not found" in result["error"]
@pytest.mark.asyncio
async def test_get_component_props_not_initialized(uninitialized_tools):
"""Test getting props when not initialized"""
result = await uninitialized_tools.get_component_props("Button")
assert "error" in result
assert result["prop_count"] == 0
@pytest.mark.asyncio
async def test_validate_component_valid(dmc_tools, mock_registry):
"""Test validating valid component props"""
props = {
"variant": "filled",
"color": "blue",
"size": "md"
}
result = await dmc_tools.validate_component("Button", props)
assert result["valid"] is True
assert len(result["errors"]) == 0
assert result["component"] == "Button"
@pytest.mark.asyncio
async def test_validate_component_invalid_prop(dmc_tools, mock_registry):
"""Test validating with invalid prop name"""
mock_registry.validate_prop.side_effect = lambda comp, prop, val: (
{"valid": False, "error": f"Unknown prop '{prop}'"} if prop == "unknownProp"
else {"valid": True}
)
props = {"unknownProp": "value"}
result = await dmc_tools.validate_component("Button", props)
assert result["valid"] is False
assert len(result["errors"]) > 0
@pytest.mark.asyncio
async def test_validate_component_missing_required(dmc_tools, mock_registry):
"""Test validating with missing required prop"""
# TextInput has required value prop
mock_registry.get_component.return_value = {
"props": {
"value": {"type": "string", "required": True}
}
}
result = await dmc_tools.validate_component("TextInput", {})
assert result["valid"] is False
assert any("required" in e.lower() for e in result["errors"])
@pytest.mark.asyncio
async def test_validate_component_not_found(dmc_tools, mock_registry):
"""Test validating nonexistent component"""
mock_registry.get_component.return_value = None
result = await dmc_tools.validate_component("Nonexistent", {"prop": "value"})
assert result["valid"] is False
assert "Unknown component" in result["errors"][0]
@pytest.mark.asyncio
async def test_validate_component_not_initialized(uninitialized_tools):
"""Test validating when not initialized"""
result = await uninitialized_tools.validate_component("Button", {})
assert result["valid"] is False
assert "not initialized" in result["errors"][0].lower()
@pytest.mark.asyncio
async def test_validate_component_skips_special_props(dmc_tools, mock_registry):
"""Test that special props (id, children, etc) are skipped"""
props = {
"id": "my-button",
"children": "Click me",
"className": "my-class",
"style": {"color": "red"},
"key": "btn-1"
}
result = await dmc_tools.validate_component("Button", props)
# Should not error on special props
assert result["valid"] is True
def test_find_similar_component(dmc_tools, mock_registry):
"""Test finding similar component names"""
# Should find Button when given 'button' (case mismatch)
similar = dmc_tools._find_similar_component("button")
assert similar == "Button"
def test_find_similar_component_prefix(dmc_tools, mock_registry):
"""Test finding similar component with prefix match"""
similar = dmc_tools._find_similar_component("Butt")
assert similar == "Button"
def test_check_common_mistakes_onclick(dmc_tools):
"""Test detection of onclick event handler mistake"""
warnings = []
dmc_tools._check_common_mistakes("Button", {"onClick": "handler"}, warnings)
assert len(warnings) > 0
assert any("callback" in w.lower() for w in warnings)
def test_check_common_mistakes_class(dmc_tools):
"""Test detection of 'class' instead of 'className'"""
warnings = []
dmc_tools._check_common_mistakes("Button", {"class": "my-class"}, warnings)
assert len(warnings) > 0
assert any("classname" in w.lower() for w in warnings)
def test_check_common_mistakes_button_href(dmc_tools):
"""Test detection of Button with href but no component prop"""
warnings = []
dmc_tools._check_common_mistakes("Button", {"href": "/link"}, warnings)
assert len(warnings) > 0
assert any("component" in w.lower() for w in warnings)
def test_initialize_with_version():
"""Test initializing tools with DMC version"""
from mcp_server.dmc_tools import DMCTools
tools = DMCTools()
with patch('mcp_server.dmc_tools.ComponentRegistry') as MockRegistry:
mock_instance = MagicMock()
mock_instance.is_loaded.return_value = True
MockRegistry.return_value = mock_instance
result = tools.initialize(dmc_version="2.5.1")
MockRegistry.assert_called_once_with("2.5.1")
assert result is True

View File

@@ -1,304 +0,0 @@
"""
Unit tests for theme management tools.
"""
import pytest
from unittest.mock import MagicMock, patch
@pytest.fixture
def theme_store():
"""Create a fresh ThemeStore instance"""
from mcp_server.theme_store import ThemeStore
store = ThemeStore()
store._themes = {} # Clear any existing themes
return store
@pytest.fixture
def theme_tools(theme_store):
"""Create ThemeTools instance with fresh store"""
from mcp_server.theme_tools import ThemeTools
return ThemeTools(store=theme_store)
def test_theme_store_init():
"""Test theme store initialization"""
from mcp_server.theme_store import ThemeStore
store = ThemeStore()
# Should have default theme
assert store.get_theme("default") is not None
def test_default_theme_structure():
"""Test default theme has required structure"""
from mcp_server.theme_store import DEFAULT_THEME
assert "name" in DEFAULT_THEME
assert "tokens" in DEFAULT_THEME
assert "colors" in DEFAULT_THEME["tokens"]
assert "spacing" in DEFAULT_THEME["tokens"]
assert "typography" in DEFAULT_THEME["tokens"]
assert "radii" in DEFAULT_THEME["tokens"]
def test_default_theme_colors():
"""Test default theme has required color tokens"""
from mcp_server.theme_store import DEFAULT_THEME
colors = DEFAULT_THEME["tokens"]["colors"]
assert "primary" in colors
assert "secondary" in colors
assert "success" in colors
assert "warning" in colors
assert "error" in colors
assert "background" in colors
assert "text" in colors
@pytest.mark.asyncio
async def test_theme_create(theme_tools):
"""Test creating a new theme"""
tokens = {
"colors": {
"primary": "#ff0000"
}
}
result = await theme_tools.theme_create("my-theme", tokens)
assert result["name"] == "my-theme"
assert "tokens" in result
assert result["tokens"]["colors"]["primary"] == "#ff0000"
@pytest.mark.asyncio
async def test_theme_create_merges_with_defaults(theme_tools):
"""Test that new theme merges with default tokens"""
tokens = {
"colors": {
"primary": "#ff0000"
}
}
result = await theme_tools.theme_create("partial-theme", tokens)
# Should have primary from our tokens
assert result["tokens"]["colors"]["primary"] == "#ff0000"
# Should inherit secondary from defaults
assert "secondary" in result["tokens"]["colors"]
@pytest.mark.asyncio
async def test_theme_create_duplicate_name(theme_tools, theme_store):
"""Test creating theme with existing name fails"""
# Create first theme
await theme_tools.theme_create("existing", {"colors": {}})
# Try to create with same name
result = await theme_tools.theme_create("existing", {"colors": {}})
assert "error" in result
assert "already exists" in result["error"]
@pytest.mark.asyncio
async def test_theme_extend(theme_tools, theme_store):
"""Test extending an existing theme"""
# Create base theme
await theme_tools.theme_create("base", {
"colors": {"primary": "#0000ff"}
})
# Extend it
result = await theme_tools.theme_extend(
base_theme="base",
overrides={"colors": {"secondary": "#00ff00"}},
new_name="extended"
)
assert result["name"] == "extended"
# Should have base primary
assert result["tokens"]["colors"]["primary"] == "#0000ff"
# Should have override secondary
assert result["tokens"]["colors"]["secondary"] == "#00ff00"
@pytest.mark.asyncio
async def test_theme_extend_nonexistent_base(theme_tools):
"""Test extending nonexistent theme fails"""
result = await theme_tools.theme_extend(
base_theme="nonexistent",
overrides={},
new_name="new"
)
assert "error" in result
assert "not found" in result["error"]
@pytest.mark.asyncio
async def test_theme_extend_default_name(theme_tools, theme_store):
"""Test extending creates default name if not provided"""
await theme_tools.theme_create("base", {"colors": {}})
result = await theme_tools.theme_extend(
base_theme="base",
overrides={}
# No new_name provided
)
assert result["name"] == "base_extended"
@pytest.mark.asyncio
async def test_theme_validate(theme_tools, theme_store):
"""Test theme validation"""
await theme_tools.theme_create("test-theme", {
"colors": {"primary": "#ff0000"},
"spacing": {"md": "16px"}
})
result = await theme_tools.theme_validate("test-theme")
assert "complete" in result or "validation" in result
@pytest.mark.asyncio
async def test_theme_validate_nonexistent(theme_tools):
"""Test validating nonexistent theme"""
result = await theme_tools.theme_validate("nonexistent")
assert "error" in result
@pytest.mark.asyncio
async def test_theme_export_css(theme_tools, theme_store):
"""Test exporting theme as CSS"""
await theme_tools.theme_create("css-theme", {
"colors": {"primary": "#ff0000"},
"spacing": {"md": "16px"}
})
result = await theme_tools.theme_export_css("css-theme")
assert "css" in result
# CSS should contain custom properties
assert "--" in result["css"]
@pytest.mark.asyncio
async def test_theme_export_css_nonexistent(theme_tools):
"""Test exporting nonexistent theme"""
result = await theme_tools.theme_export_css("nonexistent")
assert "error" in result
@pytest.mark.asyncio
async def test_theme_list(theme_tools, theme_store):
"""Test listing themes"""
await theme_tools.theme_create("theme1", {"colors": {}})
await theme_tools.theme_create("theme2", {"colors": {}})
result = await theme_tools.theme_list()
assert "themes" in result
assert "theme1" in result["themes"]
assert "theme2" in result["themes"]
@pytest.mark.asyncio
async def test_theme_activate(theme_tools, theme_store):
"""Test activating a theme"""
await theme_tools.theme_create("active-theme", {"colors": {}})
result = await theme_tools.theme_activate("active-theme")
assert result.get("active_theme") == "active-theme" or result.get("success") is True
@pytest.mark.asyncio
async def test_theme_activate_nonexistent(theme_tools):
"""Test activating nonexistent theme"""
result = await theme_tools.theme_activate("nonexistent")
assert "error" in result
def test_theme_store_get_theme(theme_store):
"""Test getting theme from store"""
from mcp_server.theme_store import DEFAULT_THEME
# Add a theme first, then retrieve it
theme_store._themes["test-theme"] = {"name": "test-theme", "tokens": {}}
result = theme_store.get_theme("test-theme")
assert result is not None
assert result["name"] == "test-theme"
def test_theme_store_list_themes(theme_store):
"""Test listing themes from store"""
result = theme_store.list_themes()
assert isinstance(result, list)
def test_deep_merge(theme_tools):
"""Test deep merging of token dicts"""
base = {
"colors": {
"primary": "#000",
"secondary": "#111"
},
"spacing": {"sm": "8px"}
}
override = {
"colors": {
"primary": "#fff"
}
}
result = theme_tools._deep_merge(base, override)
# primary should be overridden
assert result["colors"]["primary"] == "#fff"
# secondary should remain
assert result["colors"]["secondary"] == "#111"
# spacing should remain
assert result["spacing"]["sm"] == "8px"
def test_validate_tokens(theme_tools):
"""Test token validation"""
from mcp_server.theme_store import REQUIRED_TOKEN_CATEGORIES
tokens = {
"colors": {"primary": "#000"},
"spacing": {"md": "16px"},
"typography": {"fontFamily": "Inter"},
"radii": {"md": "8px"}
}
result = theme_tools._validate_tokens(tokens)
assert "complete" in result
# Check for either "missing" or "missing_required" key
assert "missing" in result or "missing_required" in result or "missing_optional" in result
def test_validate_tokens_incomplete(theme_tools):
"""Test validation of incomplete tokens"""
tokens = {
"colors": {"primary": "#000"}
# Missing spacing, typography, radii
}
result = theme_tools._validate_tokens(tokens)
# Should flag missing categories
assert result["complete"] is False or len(result.get("missing", [])) > 0

View File

@@ -1,6 +1,6 @@
{
"name": "clarity-assist",
"version": "1.2.0",
"version": "1.0.0",
"description": "Prompt optimization and requirement clarification with ND-friendly accommodations",
"author": {
"name": "Leo Miranda",

View File

@@ -0,0 +1,99 @@
# clarity-assist
Prompt optimization and requirement clarification plugin with neurodivergent-friendly accommodations.
## Overview
clarity-assist helps transform vague, incomplete, or ambiguous requests into clear, actionable specifications. It uses a structured 4-D methodology (Deconstruct, Diagnose, Develop, Deliver) and ND-friendly communication patterns.
## Commands
| Command | Description |
|---------|-------------|
| `/clarify` | Full 4-D prompt optimization for complex requests |
| `/quick-clarify` | Rapid single-pass clarification for simple requests |
## Features
### 4-D Methodology
1. **Deconstruct** - Break down the request into components
2. **Diagnose** - Analyze gaps and potential issues
3. **Develop** - Gather clarifications through structured questions
4. **Deliver** - Produce refined specification
### ND-Friendly Design
- **Option-based questioning** - Always provide 2-4 concrete choices
- **Chunked questions** - Ask 1-2 questions at a time
- **Context for questions** - Explain why you're asking
- **Conflict detection** - Check previous answers before new questions
- **Progress acknowledgment** - Summarize frequently
### Escalation Protocol
When requests are complex or users seem overwhelmed:
- Acknowledge complexity
- Offer to focus on one aspect at a time
- Build incrementally
## Installation
Add to your project's `.claude/settings.json`:
```json
{
"plugins": ["clarity-assist"]
}
```
## Usage
### Full Clarification
```
/clarify
[Your vague or complex request here]
```
### Quick Clarification
```
/quick-clarify
[Your mostly-clear request here]
```
## Configuration
No configuration required. The plugin uses sensible defaults.
## Output Format
After clarification, you receive a structured specification:
```markdown
## Clarified Request
### Summary
[Description of what will be built]
### Scope
**In Scope:** [items]
**Out of Scope:** [items]
### Requirements
[Prioritized table]
### Assumptions
[List of assumptions]
```
## Integration
For CLAUDE.md integration instructions, see `claude-md-integration.md`.
## License
MIT

View File

@@ -1,23 +1,5 @@
---
name: clarity-coach
description: Patient, structured coach helping users articulate requirements clearly. Uses neurodivergent-friendly communication patterns.
model: sonnet
permissionMode: default
disallowedTools: Write, Edit, MultiEdit
---
# Clarity Coach Agent
## Visual Output Requirements
**MANDATORY: Display header at start of every response.**
```
┌──────────────────────────────────────────────────────────────────┐
│ 💬 CLARITY-ASSIST · Clarity Coach │
└──────────────────────────────────────────────────────────────────┘
```
## Role
You are a patient, structured coach specializing in helping users articulate their requirements clearly. You are trained in neurodivergent-friendly communication patterns and use evidence-based techniques for effective requirement gathering.

View File

@@ -1,13 +1,5 @@
# /clarify - Full Prompt Optimization
## Visual Output
```
+----------------------------------------------------------------------+
| CLARITY-ASSIST - Prompt Optimization |
+----------------------------------------------------------------------+
```
## Purpose
Transform vague, incomplete, or ambiguous requests into clear, actionable specifications using the 4-D methodology with neurodivergent-friendly accommodations.
@@ -19,46 +11,127 @@ Transform vague, incomplete, or ambiguous requests into clear, actionable specif
- Tasks requiring significant context gathering
- When user seems uncertain about what they want
## Skills to Load
## 4-D Methodology
Load these skills before proceeding:
### Phase 1: Deconstruct
- `skills/4d-methodology.md` - Core 4-phase process
- `skills/nd-accommodations.md` - ND-friendly question patterns
- `skills/clarification-techniques.md` - Anti-patterns and templates
- `skills/escalation-patterns.md` - When to adjust approach
Break down the user's request into components:
## Workflow
1. **Extract explicit requirements** - What was directly stated
2. **Identify implicit assumptions** - What seems assumed but not stated
3. **Note ambiguities** - Points that could go multiple ways
4. **List dependencies** - External factors that might affect implementation
1. **Deconstruct** - Break down request into components
2. **Diagnose** - Identify gaps and conflicts
3. **Develop** - Gather clarifications via structured questions
4. **Deliver** - Present refined specification
5. **Offer RFC Creation** - For feature work, offer to save as RFC
### Phase 2: Diagnose
Analyze gaps and potential issues:
1. **Missing information** - What do we need to know?
2. **Conflicting requirements** - Do any stated goals contradict?
3. **Scope boundaries** - What's in/out of scope?
4. **Technical constraints** - Platform, language, architecture limits
### Phase 3: Develop
Gather clarifications through structured questioning:
**ND-Friendly Question Rules:**
- Present 2-4 concrete options (never open-ended alone)
- Include "Other" for custom responses
- Ask 1-2 questions at a time maximum
- Provide brief context for why you're asking
- Check for conflicts with previous answers
**Example Format:**
```
To help me understand the scope better:
**How should errors be handled?**
1. Silent logging (user sees nothing)
2. Toast notifications (brief, dismissible)
3. Modal dialogs (requires user action)
4. Other
[Context: This affects both UX and how much error-handling code we need]
```
### Phase 4: Deliver
Produce the refined specification:
```markdown
## Clarified Request
### Summary
[1-2 sentence description of what will be built]
### Scope
**In Scope:**
- [Item 1]
- [Item 2]
**Out of Scope:**
- [Item 1]
### Requirements
| # | Requirement | Priority | Notes |
|---|-------------|----------|-------|
| 1 | ... | Must | ... |
| 2 | ... | Should | ... |
### Assumptions
- [Assumption made based on conversation]
### Open Questions
- [Any remaining ambiguities, if any]
```
## Conflict Detection
Before each question, review previous answers for conflicts:
```
[Internal check]
Previous: User said "keep it simple"
Current question about: Adding configuration options
Potential conflict: More options = more complexity
→ Acknowledge: "Earlier you mentioned keeping it simple. With that in mind..."
```
## Escalation Protocol
If the request is particularly messy or the user seems overwhelmed:
1. Acknowledge the complexity
2. Offer to start with just ONE aspect
3. Build incrementally
```
"This touches a lot of areas. Rather than tackle everything at once,
let's start with [most critical piece]. Once that's clear, we can
add the other parts. Sound good?"
```
## Example Interaction
**User:** Make the app faster
**Clarify Response:**
```
Let me help clarify what kind of performance improvements you're looking for.
**What type of slowness are you experiencing?**
1. Initial page load takes too long
2. Interactions feel sluggish (clicking, typing)
3. API calls are slow
4. All of the above
5. Other
[I'm asking because each has very different solutions]
```
## Output Format
Use the Deliver phase template from `skills/4d-methodology.md` to present the clarified specification for user confirmation.
## RFC Creation Offer (Step 5)
After presenting the clarified specification, if the request appears to be a feature or enhancement:
```
---
Would you like to save this as an RFC for formal tracking?
An RFC (Request for Comments) provides:
- Structured documentation of the proposal
- Review workflow before implementation
- Integration with sprint planning
[1] Yes, create RFC from this specification
[2] No, proceed with implementation directly
```
If user selects [1]:
- Pass clarified specification to `/rfc-create`
- The Summary, Motivation, and Design sections will be populated from the clarified spec
- User can then refine the RFC and submit for review
After gathering all necessary information, use the Deliver phase format to present the clarified specification for user confirmation.

View File

@@ -1,13 +1,5 @@
# /quick-clarify - Rapid Clarification Mode
## Visual Output
```
+----------------------------------------------------------------------+
| CLARITY-ASSIST - Quick Clarify |
+----------------------------------------------------------------------+
```
## Purpose
Single-pass clarification for requests that are mostly clear but need minor disambiguation.
@@ -19,27 +11,74 @@ Single-pass clarification for requests that are mostly clear but need minor disa
- Follow-up to an already-clarified request
- Simple feature additions or bug fixes
## Skills to Load
## Process
- `skills/nd-accommodations.md` - ND-friendly question patterns
- `skills/clarification-techniques.md` - Echo and micro-summary techniques
- `skills/escalation-patterns.md` - When to escalate to full /clarify
### Step 1: Echo Understanding
## Workflow
Restate what you understood in a single sentence:
1. **Echo Understanding** - Restate in a single sentence
2. **Quick Disambiguation** - Ask ONE multiple-choice question if needed
3. **Proceed or Confirm** - Start work or offer micro-summary
```
"I understand you want [X] that does [Y]."
```
### Step 2: Quick Disambiguation
If needed, ask ONE multiple-choice question:
```
**Just to confirm:** Should this [A] or [B]?
1. [Option A with brief description]
2. [Option B with brief description]
3. Both
4. Neither / Other approach
```
### Step 3: Proceed or Confirm
If confident (>90%), proceed with a brief confirmation:
```
"Got it. I'll implement [X with chosen option]. Starting now."
```
If less confident, offer a micro-summary:
```
"Quick summary before I start:
- [Key point 1]
- [Key point 2]
- [Assumption made]
Proceed? (Or clarify anything)"
```
## Example Interaction
**User:** Add a delete button to the user list
**Quick-Clarify Response:**
```
I'll add a delete button to each row in the user list.
**Should deletion require confirmation?**
1. Yes - show confirmation dialog (Recommended)
2. No - delete immediately
3. Soft delete - mark as deleted but keep data
[Asking because accidental deletions can be problematic]
```
## Escalation to Full /clarify
If quick-clarify reveals complexity:
```
"This is more involved than it first appeared - there are
several decisions to make. Want me to switch to a more
thorough clarification process? (Just say 'yes' or 'clarify')"
```
## Output Format
No formal specification document needed. Proceed after brief confirmation, documenting assumptions inline with the work.
## Escalation
If complexity emerges, offer to switch to full `/clarify`:
```
"This is more involved than it first appeared. Want me to switch
to a more thorough clarification process?"
```
For quick-clarify, no formal specification document is needed. Just proceed with the task after brief confirmation, documenting assumptions inline with the work.

View File

@@ -1,328 +0,0 @@
# Neurodivergent Support in clarity-assist
This document describes how clarity-assist is designed to support users with neurodivergent traits, including ADHD, autism, anxiety, and other conditions that affect executive function, sensory processing, or cognitive style.
## Overview
### Purpose
clarity-assist exists to help all users transform vague or incomplete requests into clear, actionable specifications. For neurodivergent users specifically, it addresses common challenges:
- **Executive function difficulties** - Breaking down complex tasks, getting started, managing scope
- **Working memory limitations** - Keeping track of context across long conversations
- **Decision fatigue** - Facing too many open-ended choices
- **Processing style differences** - Preferring structured, predictable interactions
- **Anxiety around uncertainty** - Needing clear expectations and explicit confirmation
### Philosophy
Our design philosophy centers on three principles:
1. **Reduce cognitive load** - Never force the user to hold too much in their head at once
2. **Provide structure** - Use consistent, predictable patterns for all interactions
3. **Respect different communication styles** - Accommodate rather than assume one "right" way to think
## Features for ND Users
### 1. Reduced Cognitive Load
**Prompt Simplification**
- The 4-D methodology (Deconstruct, Diagnose, Develop, Deliver) breaks down complex requests into manageable phases
- Users never need to specify everything upfront - clarification happens incrementally
**Task Breakdown**
- Large requests are decomposed into explicit components
- Dependencies and relationships are surfaced rather than left implicit
- Scope boundaries are clearly defined (in-scope vs. out-of-scope)
### 2. Structured Output
**Consistent Formatting**
- Every clarification session produces the same structured specification:
- Summary (1-2 sentences)
- Scope (In/Out)
- Requirements table (numbered, prioritized)
- Assumptions list
- This predictability reduces the mental effort of parsing responses
**Predictable Patterns**
- Questions always follow the same format
- Progress summaries appear at regular intervals
- Escalation (simple to complex) is always offered, never forced
**Bulleted Lists Over Prose**
- Requirements are presented as scannable lists, not paragraphs
- Options are numbered for easy reference
- Key information is highlighted with bold labels
### 3. Customizable Verbosity
**Detail Levels**
- `/clarify` - Full methodology for complex requests (more thorough, more questions)
- `/quick-clarify` - Rapid mode for simple disambiguation (fewer questions, faster)
**User Control**
- Users can always say "that's enough detail" to end questioning early
- The plugin offers to break sessions into smaller parts
- "Good enough for now" is explicitly validated as an acceptable outcome
### 4. Vagueness Detection
The `UserPromptSubmit` hook automatically detects prompts that might benefit from clarification and gently suggests using `/clarify`.
**Detection Signals**
- Short prompts (< 10 words) without specific technical terms
- Vague action phrases: "help me", "fix this", "make it better"
- Ambiguous scope words: "somehow", "something", "stuff", "etc."
- Open questions without context
**Non-Blocking Approach**
- The hook never prevents you from proceeding
- It provides a suggestion with a vagueness score (percentage)
- You can disable auto-suggestions entirely via environment variable
### 5. Focus Aids
**Task Prioritization**
- Requirements are tagged as Must/Should/Could/Won't (MoSCoW)
- Critical items are separated from nice-to-haves
- Scope creep is explicitly called out and deferred
**Context Switching Warnings**
- When questions touch multiple areas, the plugin acknowledges the complexity
- Offers to focus on one aspect at a time
- Summarizes frequently to rebuild context after interruptions
## How It Works
### The UserPromptSubmit Hook
When you submit a prompt, the vagueness detection hook (`hooks/vagueness-check.sh`) runs automatically:
```
User submits prompt
|
v
Hook reads prompt from stdin
|
v
Skip if: empty, starts with /, or contains file paths
|
v
Calculate vagueness score (0.0 - 1.0)
- Short prompts: +0.3
- Vague action phrases: +0.2
- Ambiguous scope words: +0.15
- Missing technical specifics: +0.2
- Short questions without context: +0.15
|
v
If score >= threshold (default 0.6):
- Output gentle suggestion with [clarity-assist] prefix
- Show vagueness percentage
|
v
Exit 0 (always non-blocking)
```
### Example Hook Output
```
[clarity-assist] Your prompt could benefit from more clarity.
[clarity-assist] Consider running /clarity-assist to refine your request.
[clarity-assist] (Vagueness score: 65% - this is a suggestion, not a block)
```
### The 4-D Methodology
| Phase | What Happens | ND Benefit |
|-------|--------------|------------|
| **Deconstruct** | Break request into components | Shows the full picture without overwhelming |
| **Diagnose** | Identify gaps and conflicts | Surfaces hidden assumptions |
| **Develop** | Ask structured questions | Option-based, chunked, contextual |
| **Deliver** | Produce specification | Consistent, scannable format |
## Usage Examples
### Before: Vague Request
```
User: Make the app faster
```
This triggers vagueness detection because:
- Short (4 words)
- Vague action ("make")
- No specific technical context
- No measurable outcome
### After: Clarified with /clarify
```
User: /clarify Make the app faster
Claude: Let me help clarify what kind of performance improvements you're looking for.
**What type of slowness are you experiencing?**
1. Initial page load takes too long
2. Interactions feel sluggish (clicking, typing)
3. API calls are slow
4. All of the above
5. Other
[I'm asking because each has very different solutions]
```
```
User: 1
Claude: Initial page load - got it.
**What's an acceptable load time for you?**
1. Under 1 second (aggressive)
2. Under 2 seconds (standard)
3. Under 3 seconds (relaxed)
4. I don't have a specific target
[This helps me know whether to suggest quick wins or a major optimization effort]
```
After 2-3 more questions, the user receives:
```markdown
## Clarified Request
### Summary
Optimize initial page load time to under 2 seconds by addressing bundle size and initial render blocking resources.
### Scope
**In Scope:**
- Bundle analysis and splitting
- Lazy loading non-critical components
- Image optimization
- CSS/JS minification check
**Out of Scope:**
- Backend API optimization
- Database query performance
- CDN setup
### Requirements
| # | Requirement | Priority | Notes |
|---|-------------|----------|-------|
| 1 | Page loads in < 2s on 4G | Must | Measured via Lighthouse |
| 2 | Largest contentful paint < 1.5s | Should | Core Web Vital |
| 3 | No layout shift after load | Should | CLS < 0.1 |
### Assumptions
- Current load time is > 3 seconds (based on "too long" description)
- Target browsers: modern evergreen (Chrome, Firefox, Safari)
- Build tooling already configured (webpack/vite)
```
### Common Patterns Detected
| Pattern | Example | What It Triggers |
|---------|---------|------------------|
| Missing context | "Fix the bug" | "Which bug? In which file or feature?" |
| Ambiguous scope | "Update the user stuff" | "What specifically needs updating?" |
| Unclear goals | "Make it better" | "Better in what way? Faster? Cleaner? More features?" |
| Implicit knowledge | "Do it like last time" | "I don't have context from previous sessions" |
| Conflicting requests | "Keep it simple but add everything" | "Which matters more: simplicity or completeness?" |
### Quick-Clarify Example
For simpler requests that just need minor disambiguation:
```
User: /quick-clarify Add a delete button to the user list
Claude: I'll add a delete button to each row in the user list.
**Should deletion require confirmation?**
1. Yes - show confirmation dialog (Recommended)
2. No - delete immediately
3. Soft delete - mark as deleted but keep data
[Asking because accidental deletions can be problematic]
```
```
User: 1
Claude: Got it. I'll implement a delete button with confirmation dialog. Starting now.
```
## Configuration Options
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `CLARITY_ASSIST_AUTO_SUGGEST` | `true` | Enable/disable automatic vagueness detection |
| `CLARITY_ASSIST_VAGUENESS_THRESHOLD` | `0.6` | Score threshold to trigger suggestion (0.0-1.0) |
### Disabling Auto-Suggestions
If you find the vagueness detection unhelpful, disable it in your shell profile or `.env`:
```bash
export CLARITY_ASSIST_AUTO_SUGGEST=false
```
### Adjusting Sensitivity
To make detection more or less sensitive:
```bash
# More sensitive (suggests more often)
export CLARITY_ASSIST_VAGUENESS_THRESHOLD=0.4
# Less sensitive (only very vague prompts)
export CLARITY_ASSIST_VAGUENESS_THRESHOLD=0.8
```
## Tips for ND Users
### If You're Feeling Overwhelmed
- Use `/quick-clarify` instead of `/clarify` for faster interactions
- Say "let's focus on just one thing" to narrow scope
- Ask to "pause and summarize" at any point
- It's OK to say "I don't know" - the plugin will offer concrete alternatives
### If You Have Executive Function Challenges
- Start with `/clarify` even for tasks you think are simple - it helps with planning
- The structured specification can serve as a checklist
- Use the scope boundaries to prevent scope creep
### If You Prefer Detailed Structure
- The 4-D methodology provides a predictable framework
- All output follows consistent formatting
- Questions always offer numbered options
### If You Have Anxiety About Getting It Right
- The plugin validates "good enough for now" as acceptable
- You can always revisit and change earlier answers
- Assumptions are explicitly listed - nothing is hidden
## Accessibility Notes
- All output uses standard markdown that works with screen readers
- No time pressure - take as long as you need between responses
- Questions are designed to be answerable without deep context retrieval
- Visual patterns (bold, bullets, tables) create scannable structure
## Feedback
If you have suggestions for improving neurodivergent support in clarity-assist, please open an issue at:
https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/issues
Include the label `clarity-assist` and describe:
- What challenge you faced
- What would have helped
- Any specific accommodations you'd like to see

View File

@@ -1,15 +0,0 @@
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/vagueness-check.sh"
}
]
}
]
}
}

View File

@@ -1,256 +0,0 @@
#!/bin/bash
# clarity-assist vagueness detection hook
# Analyzes user prompts for vagueness and suggests /clarity-assist when beneficial
# All output MUST have [clarity-assist] prefix
# This is a NON-BLOCKING hook - always exits 0
PREFIX="[clarity-assist]"
# Check if auto-suggest is enabled (default: true)
AUTO_SUGGEST="${CLARITY_ASSIST_AUTO_SUGGEST:-true}"
if [[ "$AUTO_SUGGEST" != "true" ]]; then
exit 0
fi
# Threshold for vagueness score (default: 0.6)
THRESHOLD="${CLARITY_ASSIST_VAGUENESS_THRESHOLD:-0.6}"
# Read user prompt from stdin
PROMPT=""
if [[ -t 0 ]]; then
# No stdin available
exit 0
else
PROMPT=$(cat)
fi
# Skip empty prompts
if [[ -z "$PROMPT" ]]; then
exit 0
fi
# Skip if prompt is a command (starts with /)
if [[ "$PROMPT" =~ ^[[:space:]]*/[a-zA-Z] ]]; then
exit 0
fi
# Skip if prompt mentions specific files or paths
if [[ "$PROMPT" =~ \.(py|js|ts|sh|md|json|yaml|yml|txt|css|html|go|rs|java|c|cpp|h)([[:space:]]|$|[^a-zA-Z]) ]] || \
[[ "$PROMPT" =~ [/\\][a-zA-Z0-9_-]+[/\\] ]] || \
[[ "$PROMPT" =~ (src|lib|test|docs|plugins|hooks|commands)/ ]]; then
exit 0
fi
# Initialize vagueness score
SCORE=0
# Count words in the prompt
WORD_COUNT=$(echo "$PROMPT" | wc -w | tr -d ' ')
# ============================================================================
# Vagueness Signal Detection
# ============================================================================
# Signal 1: Very short prompts (< 10 words) are often vague
if [[ "$WORD_COUNT" -lt 10 ]]; then
# But very short specific commands are OK
if [[ "$WORD_COUNT" -lt 3 ]]; then
# Extremely short - probably intentional or a command
:
else
SCORE=$(echo "$SCORE + 0.3" | bc)
fi
fi
# Signal 2: Vague action phrases (no specific outcome)
VAGUE_ACTIONS=(
"help me"
"help with"
"do something"
"work on"
"look at"
"check this"
"fix it"
"fix this"
"make it better"
"make this better"
"improve it"
"improve this"
"update this"
"update it"
"change it"
"change this"
"can you"
"could you"
"would you"
"please help"
)
PROMPT_LOWER=$(echo "$PROMPT" | tr '[:upper:]' '[:lower:]')
for phrase in "${VAGUE_ACTIONS[@]}"; do
if [[ "$PROMPT_LOWER" == *"$phrase"* ]]; then
SCORE=$(echo "$SCORE + 0.2" | bc)
break
fi
done
# Signal 3: Ambiguous scope indicators
AMBIGUOUS_SCOPE=(
"somehow"
"something"
"somewhere"
"anything"
"whatever"
"stuff"
"things"
"etc"
"and so on"
)
for word in "${AMBIGUOUS_SCOPE[@]}"; do
if [[ "$PROMPT_LOWER" == *"$word"* ]]; then
SCORE=$(echo "$SCORE + 0.15" | bc)
break
fi
done
# Signal 4: Missing context indicators (no reference to what/where)
# Check if prompt lacks specificity markers
HAS_SPECIFICS=false
# Specific technical terms suggest clarity
SPECIFIC_MARKERS=(
"function"
"class"
"method"
"variable"
"error"
"bug"
"test"
"api"
"endpoint"
"database"
"query"
"component"
"module"
"service"
"config"
"install"
"deploy"
"build"
"run"
"execute"
"create"
"delete"
"add"
"remove"
"implement"
"refactor"
"migrate"
"upgrade"
"debug"
"log"
"exception"
"stack"
"memory"
"performance"
"security"
"auth"
"token"
"session"
"route"
"controller"
"model"
"view"
"template"
"schema"
"migration"
"commit"
"branch"
"merge"
"pull"
"push"
)
for marker in "${SPECIFIC_MARKERS[@]}"; do
if [[ "$PROMPT_LOWER" == *"$marker"* ]]; then
HAS_SPECIFICS=true
break
fi
done
if [[ "$HAS_SPECIFICS" == false ]] && [[ "$WORD_COUNT" -gt 3 ]]; then
SCORE=$(echo "$SCORE + 0.2" | bc)
fi
# Signal 5: Question without context
if [[ "$PROMPT" =~ \?$ ]] && [[ "$WORD_COUNT" -lt 8 ]]; then
# Short questions without specifics are often vague
if [[ "$HAS_SPECIFICS" == false ]]; then
SCORE=$(echo "$SCORE + 0.15" | bc)
fi
fi
# Cap score at 1.0
if (( $(echo "$SCORE > 1.0" | bc -l) )); then
SCORE="1.0"
fi
# ============================================================================
# Feature Request Detection (for RFC suggestion)
# ============================================================================
FEATURE_REQUEST=false
# Feature request phrases
FEATURE_PHRASES=(
"we should"
"it would be nice"
"feature request"
"idea:"
"suggestion:"
"what if we"
"wouldn't it be great"
"i think we need"
"we need to add"
"we could add"
"how about adding"
"can we add"
"new feature"
"enhancement"
"proposal"
)
for phrase in "${FEATURE_PHRASES[@]}"; do
if [[ "$PROMPT_LOWER" == *"$phrase"* ]]; then
FEATURE_REQUEST=true
break
fi
done
# ============================================================================
# Output suggestion if score exceeds threshold
# ============================================================================
# Compare score to threshold using bc
if (( $(echo "$SCORE >= $THRESHOLD" | bc -l) )); then
# Format score as percentage for display
SCORE_PCT=$(echo "$SCORE * 100" | bc | cut -d'.' -f1)
# Gentle, non-blocking suggestion
echo "$PREFIX Your prompt could benefit from more clarity."
echo "$PREFIX Consider running /clarify to refine your request."
echo "$PREFIX (Vagueness score: ${SCORE_PCT}% - this is a suggestion, not a block)"
# Additional RFC suggestion if feature request detected
if [[ "$FEATURE_REQUEST" == true ]]; then
echo "$PREFIX This looks like a feature idea. Consider /rfc-create to track it formally."
fi
elif [[ "$FEATURE_REQUEST" == true ]]; then
# Feature request detected but not vague - still suggest RFC
echo "$PREFIX This looks like a feature idea. Consider /rfc-create to track it formally."
fi
# Always exit 0 - this hook is non-blocking
exit 0

View File

@@ -1,76 +0,0 @@
# 4-D Methodology for Prompt Clarification
The 4-D methodology transforms vague requests into actionable specifications.
## Phase 1: Deconstruct
Break down the user's request into components:
1. **Extract explicit requirements** - What was directly stated
2. **Identify implicit assumptions** - What seems assumed but not stated
3. **Note ambiguities** - Points that could go multiple ways
4. **List dependencies** - External factors that might affect implementation
## Phase 2: Diagnose
Analyze gaps and potential issues:
1. **Missing information** - What do we need to know?
2. **Conflicting requirements** - Do any stated goals contradict?
3. **Scope boundaries** - What is in/out of scope?
4. **Technical constraints** - Platform, language, architecture limits
## Phase 3: Develop
Gather clarifications through structured questioning:
- Present 2-4 concrete options (never open-ended alone)
- Include "Other" for custom responses
- Ask 1-2 questions at a time maximum
- Provide brief context for why you are asking
- Check for conflicts with previous answers
**Example Format:**
```
To help me understand the scope better:
**How should errors be handled?**
1. Silent logging (user sees nothing)
2. Toast notifications (brief, dismissible)
3. Modal dialogs (requires user action)
4. Other
[Context: This affects both UX and how much error-handling code we need]
```
## Phase 4: Deliver
Produce the refined specification:
```markdown
## Clarified Request
### Summary
[1-2 sentence description of what will be built]
### Scope
**In Scope:**
- [Item 1]
- [Item 2]
**Out of Scope:**
- [Item 1]
### Requirements
| # | Requirement | Priority | Notes |
|---|-------------|----------|-------|
| 1 | ... | Must | ... |
| 2 | ... | Should | ... |
### Assumptions
- [Assumption made based on conversation]
### Open Questions
- [Any remaining ambiguities, if any]
```

View File

@@ -1,86 +0,0 @@
# Clarification Techniques
Structured approaches for disambiguating user requests.
## Anti-Patterns to Detect
### Vague Requests
**Triggers:** "improve", "fix", "update", "change", "better", "faster", "cleaner"
**Response:** Ask for specific metrics or outcomes
### Scope Creep Signals
**Triggers:** "while you're at it", "also", "might as well", "and another thing"
**Response:** Acknowledge, then isolate: "I'll note that for after the main task"
### Assumption Gaps
**Triggers:** References to "the" thing (which thing?), "it" (what?), "there" (where?)
**Response:** Echo back specific understanding
### Conflicting Requirements
**Triggers:** "Simple but comprehensive", "Fast but thorough", "Minimal but complete"
**Response:** Prioritize: "Which matters more: simplicity or completeness?"
## Question Templates
### For Unclear Purpose
```
**What problem does this solve?**
1. [Specific problem A]
2. [Specific problem B]
3. Combination
4. Different problem: ____
```
### For Missing Scope
```
**What should this include?**
- [ ] Feature A
- [ ] Feature B
- [ ] Feature C
- [ ] Other: ____
```
### For Ambiguous Behavior
```
**When [trigger event], what should happen?**
1. [Behavior option A]
2. [Behavior option B]
3. Nothing (ignore)
4. Depends on: ____
```
### For Technical Decisions
```
**Implementation approach:**
1. [Approach A] - pros: X, cons: Y
2. [Approach B] - pros: X, cons: Y
3. Let me decide based on codebase
4. Need more info about: ____
```
## Echo Understanding Technique
Before diving into questions, restate understanding:
```
"I understand you want [X] that does [Y]."
```
This validates comprehension and gives user a chance to correct early.
## Micro-Summary Technique
For quick confirmations before proceeding:
```
"Quick summary before I start:
- [Key point 1]
- [Key point 2]
- [Assumption made]
Proceed? (Or clarify anything)"
```

View File

@@ -1,57 +0,0 @@
# Escalation Patterns
Guidelines for when to escalate between clarification modes.
## Quick-Clarify to Full Clarify
Escalate when quick-clarify reveals unexpected complexity:
```
"This is more involved than it first appeared - there are
several decisions to make. Want me to switch to a more
thorough clarification process? (Just say 'yes' or 'clarify')"
```
### Triggers for Escalation
- Multiple ambiguities discovered during quick pass
- User's answer reveals hidden dependencies
- Scope expands beyond original understanding
- Technical constraints emerge that need discussion
- Conflicting requirements surface
## Full Clarify to Incremental
When user is overwhelmed by full 4-D process:
```
"This touches a lot of areas. Rather than tackle everything at once,
let's start with [most critical piece]. Once that's clear, we can
add the other parts. Sound good?"
```
### Signs of Overwhelm
- Long pauses or hesitation
- "I don't know" responses
- Requesting breaks
- Contradicting earlier answers
- Expressing frustration
## Choosing Initial Mode
### Use /quick-clarify When
- Request is fairly clear, just one or two ambiguities
- User is in a hurry
- Follow-up to an already-clarified request
- Simple feature additions or bug fixes
- Confidence is high (>90%)
### Use /clarify When
- Complex multi-step requests
- Requirements with multiple possible interpretations
- Tasks requiring significant context gathering
- User seems uncertain about what they want
- First time working on this feature/area

View File

@@ -1,74 +0,0 @@
# Neurodivergent-Friendly Accommodations
Guidelines for making clarification interactions accessible and comfortable for neurodivergent users.
## Core Principles
### Reduce Cognitive Load
- Maximum 4 options per question
- Always include "Other" escape hatch
- Provide examples, not just descriptions
- Use numbered lists for easy reference
### Support Working Memory
- Summarize frequently
- Reference earlier decisions explicitly
- Do not assume user remembers context from many turns ago
- Echo back understanding before proceeding
### Allow Processing Time
- Do not rapid-fire questions
- Validate answers before moving on
- Offer to revisit or change earlier answers
- One question block at a time
### Manage Overwhelm
- Offer to break into smaller sessions
- Prioritize must-haves vs nice-to-haves
- Provide "good enough for now" options
- Acknowledge complexity openly
## Question Formatting Rules
**Always do:**
```
**How should errors be handled?**
1. Silent logging (user sees nothing)
2. Toast notifications (brief, dismissible)
3. Modal dialogs (requires user action)
4. Other
[Context: This affects both UX and error-handling complexity]
```
**Never do:**
```
How do you want to handle errors? There are many approaches...
```
## Conflict Acknowledgment
Before asking about something that might conflict with a previous answer:
```
[Internal check]
Previous: User said "keep it simple"
Current question about: Adding configuration options
Potential conflict: More options = more complexity
```
Then acknowledge: "Earlier you mentioned keeping it simple. With that in mind..."
## Escalation for Overwhelm
If the request is particularly complex or user seems overwhelmed:
1. Acknowledge the complexity openly
2. Offer to start with just ONE aspect
3. Build incrementally
```
"This touches a lot of areas. Rather than tackle everything at once,
let's start with [most critical piece]. Once that's clear, we can
add the other parts. Sound good?"
```

View File

@@ -1,7 +1,7 @@
{
"name": "claude-config-maintainer",
"version": "1.2.0",
"description": "Maintains and optimizes CLAUDE.md and settings.local.json configuration files for Claude Code projects",
"version": "1.0.0",
"description": "Maintains and optimizes CLAUDE.md configuration files for Claude Code projects",
"author": {
"name": "Leo Miranda",
"email": "leobmiranda@gmail.com"
@@ -14,9 +14,7 @@
"configuration",
"optimization",
"claude-md",
"developer-tools",
"settings",
"permissions"
"developer-tools"
],
"commands": ["./commands/"]
}

View File

@@ -0,0 +1,99 @@
# Claude Config Maintainer
A Claude Code plugin for creating and maintaining optimized CLAUDE.md configuration files.
## Overview
CLAUDE.md files provide instructions to Claude Code when working with a project. This plugin helps you:
- **Analyze** existing CLAUDE.md files for improvement opportunities
- **Optimize** structure, clarity, and conciseness
- **Initialize** new CLAUDE.md files with project-specific content
## Installation
This plugin is part of the Leo Claude Marketplace. Install the marketplace and the plugin will be available.
## Commands
### `/config-analyze`
Analyze your CLAUDE.md and get a detailed report with scores and recommendations.
```
/config-analyze
```
### `/config-optimize`
Automatically optimize your CLAUDE.md based on best practices.
```
/config-optimize
```
### `/config-init`
Create a new CLAUDE.md tailored to your project.
```
/config-init
```
## Best Practices
A good CLAUDE.md should be:
- **Clear** - Easy to understand at a glance
- **Concise** - No unnecessary content
- **Complete** - All essential information included
- **Current** - Up to date with the project
### Recommended Structure
```markdown
# CLAUDE.md
## Project Overview
What does this project do?
## Quick Start
Essential build/test/run commands.
## Critical Rules
What must Claude NEVER do?
## Architecture (optional)
Key technical decisions.
## Common Operations (optional)
Frequent tasks and workflows.
```
### Length Guidelines
| Project Size | Recommended Lines |
|-------------|------------------|
| Small | 50-100 |
| Medium | 100-200 |
| Large | 200-400 |
## Scoring System
The analyzer scores CLAUDE.md files on:
- **Structure** (25 pts) - Organization and navigation
- **Clarity** (25 pts) - Clear, unambiguous instructions
- **Completeness** (25 pts) - Essential sections present
- **Conciseness** (25 pts) - Efficient information density
Target score: **70+** for effective Claude Code usage.
## Tips
1. Run `/config-analyze` periodically to maintain quality
2. Update CLAUDE.md when adding major features
3. Keep critical rules prominent and clear
4. Include examples where they add clarity
5. Remove generic advice that applies to all projects
## Contributing
This plugin is part of the personal-projects/leo-claude-mktplace repository.

View File

@@ -1,25 +1,12 @@
---
name: maintainer
description: CLAUDE.md optimization and maintenance agent
model: sonnet
permissionMode: acceptEdits
skills: visual-header, settings-optimization
---
# CLAUDE.md Maintainer Agent
You are the **Maintainer Agent** - a specialist in creating and optimizing CLAUDE.md configuration files for Claude Code projects. Your role is to ensure CLAUDE.md files are clear, concise, well-structured, and follow best practices.
## Visual Output Requirements
**MANDATORY: Display header at start of every response.**
```
┌──────────────────────────────────────────────────────────────────┐
│ ⚙️ CONFIG-MAINTAINER · CLAUDE.md Optimization │
└──────────────────────────────────────────────────────────────────┘
```
## Your Personality
**Optimization-Focused:**
@@ -117,54 +104,7 @@ Report plugin coverage percentage and offer to add missing integrations:
- Display the integration content that would be added
- Ask user for confirmation before modifying CLAUDE.md
### 2. Audit Settings Files
When auditing settings files, perform:
#### A. Permission Analysis
Read `.claude/settings.local.json` (primary) and check `.claude/settings.json` and `~/.claude.json` project entries (secondary).
Evaluate using `skills/settings-optimization.md`:
**Redundancy:**
- Duplicate entries in allow/deny arrays
- Subset patterns covered by broader patterns
- Patterns that could be merged
**Coverage:**
- Common safe tools missing from allow list
- MCP server tools not covered
- Directory scopes with no matching permission
**Safety Alignment:**
- Deny rules cover secrets and destructive commands
- Allow rules don't bypass active review layers
- No overly broad patterns without justification
**Profile Fit:**
- Compare against recommended profile for the project's review architecture
- Identify specific additions/removals to reach target profile
#### B. Review Layer Verification
Before recommending auto-allow patterns, verify active review layers:
1. Read `plugins/*/hooks/hooks.json` for each installed plugin
2. Map hook types (PreToolUse, PostToolUse) to tool matchers (Write, Edit, Bash)
3. Confirm plugins are listed in `.claude-plugin/marketplace.json`
4. Only recommend auto-allow for scopes covered by ≥2 verified review layers
#### C. Settings Efficiency Score (100 points)
| Category | Points |
|----------|--------|
| Redundancy | 25 |
| Coverage | 25 |
| Safety Alignment | 25 |
| Profile Fit | 25 |
### 3. Optimize CLAUDE.md Structure
### 2. Optimize CLAUDE.md Structure
**Recommended Structure:**
@@ -199,7 +139,7 @@ Common issues and solutions.
- Use headers that scan easily
- Include examples where they add clarity
### 4. Apply Best Practices
### 3. Apply Best Practices
**DO:**
- Use clear, direct language
@@ -216,7 +156,7 @@ Common issues and solutions.
- Add generic advice that applies to all projects
- Use emojis unless project requires them
### 5. Generate Improvement Reports
### 4. Generate Improvement Reports
After analyzing a CLAUDE.md, provide:
@@ -252,7 +192,7 @@ Suggested Actions:
Would you like me to implement these improvements?
```
### 6. Insert Plugin Integrations
### 5. Insert Plugin Integrations
When adding plugin integration content to CLAUDE.md:
@@ -287,7 +227,7 @@ Add this integration to CLAUDE.md?
- Allow users to skip specific plugins they don't want documented
- Preserve existing CLAUDE.md structure and content
### 7. Create New CLAUDE.md Files
### 6. Create New CLAUDE.md Files
When creating a new CLAUDE.md:
@@ -327,39 +267,6 @@ Every CLAUDE.md should have:
1. **Project Overview** - What is this?
2. **Quick Start** - How do I build/test/run?
3. **Important Rules** - What must I NOT do?
4. **Pre-Change Protocol** - Mandatory dependency check before code changes
### Pre-Change Protocol Section (MANDATORY)
**This section is REQUIRED in every CLAUDE.md.** It ensures Claude performs comprehensive dependency analysis before making any code changes.
```markdown
## ⛔ MANDATORY: Before Any Code Change
**Claude MUST show this checklist BEFORE editing any file:**
### 1. Impact Search Results
Run and show output of:
```bash
grep -rn "PATTERN" --include="*.sh" --include="*.md" --include="*.json" --include="*.py" | grep -v ".git"
```
### 2. Files That Will Be Affected
Numbered list of every file to be modified, with the specific change for each.
### 3. Files Searched But Not Changed (and why)
Proof that related files were checked and determined unchanged.
### 4. Documentation That References This
List of docs that mention this feature/script/function.
**User verifies this list before Claude proceeds. If Claude skips this, stop immediately.**
### After Changes
Run the same grep and show results proving no references remain unaddressed.
```
**When analyzing a CLAUDE.md, flag as HIGH priority issue if this section is missing.**
### Optional Sections (as needed)

View File

@@ -1,6 +1,6 @@
## CLAUDE.md Maintenance (claude-config-maintainer)
This project uses the **claude-config-maintainer** plugin to analyze and optimize CLAUDE.md and settings.local.json configuration files.
This project uses the **claude-config-maintainer** plugin to analyze and optimize CLAUDE.md configuration files.
### Available Commands
@@ -9,13 +9,8 @@ This project uses the **claude-config-maintainer** plugin to analyze and optimiz
| `/config-analyze` | Analyze CLAUDE.md for optimization opportunities with 100-point scoring |
| `/config-optimize` | Automatically optimize CLAUDE.md structure and content |
| `/config-init` | Initialize a new CLAUDE.md file for a project |
| `/config-diff` | Track CLAUDE.md changes over time with behavioral impact analysis |
| `/config-lint` | Lint CLAUDE.md for anti-patterns and best practices (31 rules) |
| `/config-audit-settings` | Audit settings.local.json permissions with 100-point scoring |
| `/config-optimize-settings` | Optimize permission patterns and apply named profiles |
| `/config-permissions-map` | Visual map of review layers and permission coverage |
### CLAUDE.md Scoring System
### Scoring System
The analysis uses a 100-point scoring system across four categories:
@@ -26,31 +21,10 @@ The analysis uses a 100-point scoring system across four categories:
| Completeness | 25 | Overview, quick start, critical rules, workflows |
| Conciseness | 25 | Efficiency, no repetition, appropriate length |
### Settings Scoring System
The settings audit uses a 100-point scoring system across four categories:
| Category | Points | What It Measures |
|----------|--------|------------------|
| Redundancy | 25 | No duplicates, no subset patterns, efficient rules |
| Coverage | 25 | Common tools allowed, MCP servers covered |
| Safety Alignment | 25 | Deny rules for secrets/destructive ops, review layers verified |
| Profile Fit | 25 | Alignment with recommended profile for review layer count |
### Permission Profiles
| Profile | Use Case |
|---------|----------|
| `conservative` | New users, minimal auto-allow, prompts for most writes |
| `reviewed` | Projects with 2+ review layers (code-sentinel, doc-guardian, PR review) |
| `autonomous` | Trusted CI/sandboxed environments only |
### Usage Guidelines
- Run `/config-analyze` periodically to assess CLAUDE.md quality
- Run `/config-audit-settings` to check permission efficiency
- Target a score of **70+/100** for effective Claude Code operation
- Address HIGH priority issues first when optimizing
- Use `/config-init` when setting up new projects to start with best practices
- Use `/config-permissions-map` to visualize review layer coverage
- Re-analyze after making changes to verify improvements

View File

@@ -4,18 +4,17 @@ description: Analyze CLAUDE.md for optimization opportunities and plugin integra
# Analyze CLAUDE.md
Analyze your CLAUDE.md and provide a scored report with recommendations.
This command analyzes your project's CLAUDE.md file and provides a detailed report on optimization opportunities and plugin integration status.
## Skills to Load
## What This Command Does
- skills/visual-header.md
- skills/analysis-workflow.md
- skills/optimization-patterns.md
- skills/pre-change-protocol.md
## Visual Output
Display: `CONFIG-MAINTAINER - CLAUDE.md Analysis`
1. **Read CLAUDE.md** - Locates and reads the project's CLAUDE.md file
2. **Analyze Structure** - Evaluates organization, headers, and flow
3. **Check Content** - Reviews clarity, completeness, and conciseness
4. **Identify Issues** - Finds redundancy, verbosity, and missing sections
5. **Detect Active Plugins** - Identifies marketplace plugins enabled in the project
6. **Check Plugin Integration** - Verifies CLAUDE.md references active plugins
7. **Generate Report** - Provides scored assessment with recommendations
## Usage
@@ -23,27 +22,165 @@ Display: `CONFIG-MAINTAINER - CLAUDE.md Analysis`
/config-analyze
```
## Workflow
Or invoke the maintainer agent directly:
1. Locate and parse CLAUDE.md
2. Evaluate structure, clarity, completeness, conciseness
3. Find redundancy, verbosity, missing sections
4. Detect active marketplace plugins
5. Verify plugin integration in CLAUDE.md
6. Generate scored report with recommendations
```
Analyze the CLAUDE.md file in this project
```
## Scoring (100 points)
## Analysis Criteria
| Category | Points |
|----------|--------|
| Structure | 25 |
| Clarity | 25 |
| Completeness | 25 |
| Conciseness | 25 |
### Structure (25 points)
- Logical section ordering
- Clear header hierarchy
- Easy navigation
- Appropriate grouping
### Clarity (25 points)
- Clear instructions
- Good examples
- Unambiguous language
- Appropriate detail level
### Completeness (25 points)
- Project overview present
- Quick start commands documented
- Critical rules highlighted
- Key workflows covered
### Conciseness (25 points)
- No unnecessary repetition
- Efficient information density
- Appropriate length for project size
- No generic filler content
## Plugin Integration Analysis
After the content analysis, the command detects and analyzes marketplace plugin integration:
### Detection Method
1. **Read `.claude/settings.local.json`** - Check for enabled MCP servers
2. **Map MCP servers to plugins** - Use marketplace registry to identify active plugins:
- `gitea` → projman
- `netbox` → cmdb-assistant
3. **Check for hooks** - Identify hook-based plugins (project-hygiene)
4. **Scan CLAUDE.md** - Look for plugin integration content
### Plugin Coverage Scoring
For each detected plugin, verify CLAUDE.md contains:
- Plugin section header or mention
- Available commands documentation
- MCP tools reference (if applicable)
- Usage guidelines
Coverage is reported as percentage: `(plugins referenced / plugins detected) * 100`
## Expected Output
```
CLAUDE.md Analysis Report
=========================
File: /path/to/project/CLAUDE.md
Lines: 245
Last Modified: 2025-01-18
Overall Score: 72/100
Category Scores:
- Structure: 20/25 (Good)
- Clarity: 18/25 (Good)
- Completeness: 22/25 (Excellent)
- Conciseness: 12/25 (Needs Work)
Strengths:
+ Clear project overview with good context
+ Critical rules prominently displayed
+ Comprehensive coverage of workflows
Issues Found:
1. [HIGH] Verbose explanations (lines 45-78)
Section "Running Tests" has 34 lines that could be 8 lines.
Impact: Harder to scan, important info buried
2. [MEDIUM] Duplicate content (lines 102-115, 189-200)
Same git workflow documented twice.
Impact: Maintenance burden, inconsistency risk
3. [MEDIUM] Missing Quick Start section
No clear "how to get started" instructions.
Impact: Slower onboarding for Claude
4. [LOW] Inconsistent header formatting
Mix of "## Title" and "## Title:" styles.
Impact: Minor readability issue
Recommendations:
1. Add Quick Start section at top (priority: high)
2. Condense Testing section to essentials (priority: high)
3. Remove duplicate git workflow (priority: medium)
4. Standardize header formatting (priority: low)
Estimated improvement: 15-20 points after changes
---
Plugin Integration Analysis
===========================
Detected Active Plugins:
✓ projman (via gitea MCP server)
✓ cmdb-assistant (via netbox MCP server)
✓ project-hygiene (via PostToolUse hook)
Plugin Coverage: 33% (1/3 plugins referenced)
✓ projman - Referenced in CLAUDE.md
✗ cmdb-assistant - NOT referenced
✗ project-hygiene - NOT referenced
Missing Integration Content:
1. cmdb-assistant
Add infrastructure management commands and NetBox MCP tools reference.
2. project-hygiene
Add cleanup hook documentation and configuration options.
---
Would you like me to:
[1] Implement all content recommendations
[2] Add missing plugin integrations to CLAUDE.md
[3] Do both (recommended)
[4] Show preview of changes first
```
## When to Use
Run `/config-analyze` when:
- Setting up a new project with existing CLAUDE.md
- CLAUDE.md feels too long or hard to use
- Claude seems to miss instructions
- Before major project changes
- Periodic maintenance (quarterly)
- After installing new marketplace plugins
- When Claude doesn't seem to use available plugin tools
## Follow-Up Actions
1. Implement content recommendations
2. Add missing plugin integrations
3. Do both (recommended)
4. Show preview first
After analysis, you can:
- Run `/config-optimize` to automatically improve the file
- Manually address specific issues
- Request detailed recommendations for any section
- Compare with best practice templates
## Tips
- Run analysis after significant project changes
- Address HIGH priority issues first
- Keep scores above 70/100 for best results
- Re-analyze after making changes to verify improvement

View File

@@ -1,204 +0,0 @@
---
name: config-audit-settings
description: Audit settings.local.json for permission optimization opportunities
---
# /config-audit-settings
Audit Claude Code `settings.local.json` permissions with 100-point scoring across redundancy, coverage, safety alignment, and profile fit.
## Skills to Load
Before executing, load:
- `skills/visual-header.md`
- `skills/settings-optimization.md`
## Visual Output
```
+-----------------------------------------------------------------+
| CONFIG-MAINTAINER - Settings Audit |
+-----------------------------------------------------------------+
```
## Usage
```
/config-audit-settings # Full audit with recommendations
/config-audit-settings --diagram # Include Mermaid diagram of review layer coverage
```
## Workflow
### Step 1: Locate Settings Files
Search in order:
1. `.claude/settings.local.json` (primary target)
2. `.claude/settings.json` (shared config)
3. `~/.claude.json` project entry (legacy)
Report which format is in use.
### Step 2: Parse Permission Arrays
Extract and analyze:
- `permissions.allow` array
- `permissions.deny` array
- `permissions.ask` array (if present)
- Legacy `allowedTools` array (if legacy format)
### Step 3: Run Pattern Consolidation Analysis
Using `settings-optimization.md` Section 3, detect:
| Check | Description |
|-------|-------------|
| Duplicates | Exact same pattern appearing multiple times |
| Subsets | Narrower patterns covered by broader ones |
| Merge candidates | 4+ similar patterns that could be consolidated |
| Overly broad | Unscoped tool permissions (e.g., `Bash` without pattern) |
| Stale entries | Patterns referencing non-existent paths |
| Conflicts | Same pattern in both allow and deny |
### Step 4: Detect Active Marketplace Hooks
Read `plugins/*/hooks/hooks.json` files:
```bash
# Check each plugin's hooks
plugins/code-sentinel/hooks/hooks.json # PreToolUse security
plugins/doc-guardian/hooks/hooks.json # PostToolUse drift detection
plugins/project-hygiene/hooks/hooks.json # PostToolUse cleanup
plugins/data-platform/hooks/hooks.json # PostToolUse schema diff
plugins/contract-validator/hooks/hooks.json # Plugin validation
```
Parse each to identify:
- Hook event type (PreToolUse, PostToolUse)
- Tool matchers (Write, Edit, MultiEdit, Bash)
- Whether hook is command type (reliable) or prompt type (unreliable)
### Step 5: Map Review Layers to Directory Scopes
For each directory scope in `settings-optimization.md` Section 4:
1. Count how many review layers are verified active
2. Determine if auto-allow is justified (≥2 layers required)
3. Note any scopes that lack coverage
### Step 6: Compare Against Recommended Profile
Based on review layer count:
- 0-1 layers: Recommend `conservative` profile
- 2+ layers: Recommend `reviewed` profile
- CI/sandboxed: May recommend `autonomous` profile
Calculate profile fit percentage.
### Step 7: Generate Scored Report
Calculate scores using `settings-optimization.md` Section 6.
## Output Format
```
Settings Efficiency Score: XX/100
Redundancy: XX/25
Coverage: XX/25
Safety Alignment: XX/25
Profile Fit: XX/25
Current Profile: [closest match or "custom"]
Recommended Profile: [target based on review layers]
Issues Found:
🔴 CRITICAL: [description]
🟠 HIGH: [description]
🟡 MEDIUM: [description]
🔵 LOW: [description]
Active Review Layers Detected:
✓ code-sentinel (PreToolUse: Write|Edit|MultiEdit)
✓ doc-guardian (PostToolUse: Write|Edit|MultiEdit)
✓ project-hygiene (PostToolUse: Write|Edit)
✗ data-platform schema-diff (not detected)
Recommendations:
1. [specific action with pattern]
2. [specific action with pattern]
...
Follow-Up Actions:
1. Run /config-optimize-settings to apply recommendations
2. Run /config-optimize-settings --dry-run to preview first
3. Run /config-optimize-settings --profile=reviewed to apply profile
```
## Diagram Output (--diagram flag)
When `--diagram` is specified, generate a Mermaid flowchart showing:
**Before generating:** Read `/mnt/skills/user/mermaid-diagrams/SKILL.md` for diagram requirements.
**Diagram structure:**
- Left column: File operation types (Write, Edit, Bash)
- Middle: Review layers that intercept each operation
- Right column: Current permission status (auto-allowed, prompted, denied)
**Color coding:**
- PreToolUse hooks: Blue
- PostToolUse hooks: Green
- Sprint Approval: Amber
- PR Review: Purple
Example structure:
```mermaid
flowchart LR
subgraph Operations
W[Write]
E[Edit]
B[Bash]
end
subgraph Review Layers
CS[code-sentinel]
DG[doc-guardian]
PR[pr-review]
end
subgraph Permission
A[Auto-allowed]
P[Prompted]
D[Denied]
end
W --> CS
W --> DG
E --> CS
E --> DG
CS --> A
DG --> A
B --> P
classDef preHook fill:#e3f2fd
classDef postHook fill:#e8f5e9
classDef prReview fill:#f3e5f5
class CS preHook
class DG postHook
class PR prReview
```
## Issue Severity Levels
| Severity | Icon | Examples |
|----------|------|----------|
| CRITICAL | 🔴 | Unscoped `Bash` in allow, missing deny for secrets |
| HIGH | 🟠 | Overly broad patterns, missing MCP coverage |
| MEDIUM | 🟡 | Subset redundancy, merge candidates |
| LOW | 🔵 | Exact duplicates, minor optimizations |
## DO NOT
- Modify any files (this is audit only)
- Recommend `autonomous` profile unless explicitly sandboxed environment
- Recommend auto-allow for scopes with <2 verified review layers
- Skip hook verification before making recommendations

View File

@@ -1,48 +0,0 @@
---
description: Show diff between current CLAUDE.md and last commit
---
# Compare CLAUDE.md Changes
Show differences between CLAUDE.md versions to track configuration drift.
## Skills to Load
- skills/visual-header.md
- skills/diff-analysis.md
## Visual Output
Display: `CONFIG-MAINTAINER - CLAUDE.md Diff`
## Usage
```
/config-diff # Working vs last commit
/config-diff --commit=abc1234 # Working vs specific commit
/config-diff --from=v1.0 --to=v2.0 # Compare two commits
/config-diff --section="Critical Rules" # Specific section only
```
## Workflow
1. Find project's CLAUDE.md file
2. Show diff against target revision
3. Group changes by affected sections
4. Explain behavioral implications
## Options
| Option | Description |
|--------|-------------|
| `--commit=REF` | Compare against specific commit |
| `--from=REF` | Starting point |
| `--to=REF` | Ending point (default: HEAD) |
| `--section=NAME` | Show only specific section |
| `--stat` | Statistics only |
## When to Use
- Before committing CLAUDE.md changes
- Reviewing changes after pull
- Debugging unexpected behavior

View File

@@ -1,48 +0,0 @@
---
description: Lint CLAUDE.md for common anti-patterns and best practices
---
# Lint CLAUDE.md
Check CLAUDE.md against best practices and detect common anti-patterns.
## Skills to Load
- skills/visual-header.md
- skills/lint-rules.md
## Visual Output
Display: `CONFIG-MAINTAINER - CLAUDE.md Lint`
## Usage
```
/config-lint # Full lint
/config-lint --fix # Auto-fix issues
/config-lint --rules=security # Check specific category
```
## Workflow
1. Parse markdown structure and hierarchy
2. Check for hardcoded paths, secrets, sensitive data
3. Identify content anti-patterns
4. Verify consistent formatting
5. Generate report with fix suggestions
## Options
| Option | Description |
|--------|-------------|
| `--fix` | Auto-fix issues |
| `--rules=LIST` | Check specific categories |
| `--ignore=LIST` | Skip specified rules |
| `--severity=LEVEL` | Filter by severity |
| `--strict` | Treat warnings as errors |
## When to Use
- Before committing CLAUDE.md changes
- During code review
- Periodically as maintenance

View File

@@ -1,243 +0,0 @@
---
name: config-optimize-settings
description: Optimize settings.local.json permissions based on audit recommendations
---
# /config-optimize-settings
Optimize Claude Code `settings.local.json` permission patterns and apply named profiles.
## Skills to Load
Before executing, load:
- `skills/visual-header.md`
- `skills/settings-optimization.md`
- `skills/pre-change-protocol.md`
## Visual Output
```
+-----------------------------------------------------------------+
| CONFIG-MAINTAINER - Settings Optimization |
+-----------------------------------------------------------------+
```
## Usage
```
/config-optimize-settings # Apply audit recommendations
/config-optimize-settings --dry-run # Preview only, no changes
/config-optimize-settings --profile=reviewed # Apply named profile
/config-optimize-settings --consolidate-only # Only merge/dedupe, no new rules
```
## Options
| Option | Description |
|--------|-------------|
| `--dry-run` | Preview changes without applying |
| `--profile=NAME` | Apply named profile (`conservative`, `reviewed`, `autonomous`) |
| `--consolidate-only` | Only deduplicate and merge patterns, don't add new rules |
| `--no-backup` | Skip backup (not recommended) |
## Workflow
### Step 1: Run Audit Analysis
Execute the same analysis as `/config-audit-settings`:
1. Locate settings file
2. Parse permission arrays
3. Detect issues (duplicates, subsets, merge candidates, etc.)
4. Verify active review layers
5. Calculate current score
### Step 2: Generate Optimization Plan
Based on audit results, create a change plan:
**For `--consolidate-only`:**
- Remove exact duplicates
- Remove subset patterns covered by broader patterns
- Merge similar patterns (4+ threshold)
- Remove stale patterns for non-existent paths
- Remove conflicting allow entries that are already denied
**For `--profile=NAME`:**
- Calculate diff between current permissions and target profile
- Show additions and removals
- Preserve any custom deny rules not in profile
**For default (full optimization):**
- Apply all consolidation changes
- Add recommended patterns based on verified review layers
- Suggest profile alignment if appropriate
### Step 3: Show Before/After Preview
**MANDATORY:** Always show preview before applying changes.
```
Current Settings:
allow: [12 patterns]
deny: [4 patterns]
Proposed Changes:
REMOVE from allow (redundant):
- Write(plugins/projman/*) [covered by Write(plugins/**)]
- Write(plugins/git-flow/*) [covered by Write(plugins/**)]
- Bash(git status) [covered by Bash(git *)]
ADD to allow (recommended):
+ Bash(npm *) [2 review layers active]
+ Bash(pytest *) [2 review layers active]
ADD to deny (security):
+ Bash(curl * | bash*) [missing safety rule]
After Optimization:
allow: [10 patterns]
deny: [5 patterns]
Score Impact: 67/100 → 85/100 (+18 points)
```
### Step 4: Request User Approval
Ask for confirmation before proceeding:
```
Apply these changes to .claude/settings.local.json?
[1] Yes, apply changes
[2] No, cancel
[3] Apply partial (select which changes)
```
### Step 5: Create Backup
**Before any write operation:**
```bash
# Backup location
.claude/backups/settings.local.json.{YYYYMMDD-HHMMSS}
```
Create the `.claude/backups/` directory if it doesn't exist.
### Step 6: Apply Changes
Write the optimized `settings.local.json` file.
### Step 7: Verify
Re-read the file and re-calculate the score to confirm improvement.
```
Optimization Complete!
Backup saved: .claude/backups/settings.local.json.20260202-143022
Settings Efficiency Score: 85/100 (+18 from 67)
Redundancy: 25/25 (+8)
Coverage: 22/25 (+5)
Safety Alignment: 23/25 (+3)
Profile Fit: 15/25 (+2)
Changes applied:
- Removed 3 redundant patterns
- Added 2 recommended patterns
- Added 1 safety deny rule
```
## Profile Application
When using `--profile=NAME`:
### `conservative`
```
Switching to conservative profile...
This profile:
- Allows: Read, Glob, Grep, LS, basic Bash commands
- Allows: Write/Edit only for docs/
- Denies: .env*, secrets/, rm -rf, sudo
All other Write/Edit operations will prompt for approval.
```
### `reviewed`
```
Switching to reviewed profile...
Prerequisites verified:
✓ code-sentinel hook active (PreToolUse)
✓ doc-guardian hook active (PostToolUse)
✓ 2+ review layers detected
This profile:
- Allows: All file operations (Edit, Write, MultiEdit)
- Allows: Scoped Bash commands (git, npm, python, etc.)
- Denies: .env*, secrets/, rm -rf, sudo, curl|bash
```
### `autonomous`
```
⚠️ WARNING: Autonomous profile requested
This profile allows unscoped Bash execution.
Only use in fully sandboxed environments (CI, containers).
Confirm this is a sandboxed environment?
[1] Yes, this is sandboxed - apply autonomous profile
[2] No, cancel
```
## Safety Rules
1. **ALWAYS backup before writing** (unless `--no-backup`)
2. **NEVER remove deny rules without explicit confirmation**
3. **NEVER add unscoped `Bash` to allow** — always use scoped patterns
4. **Preview is MANDATORY** before applying changes
5. **Verify review layers** before recommending broad permissions
## Output Format
### Dry Run Output
```
+-----------------------------------------------------------------+
| CONFIG-MAINTAINER - Settings Optimization |
+-----------------------------------------------------------------+
DRY RUN - No changes will be made
[... preview content ...]
To apply these changes, run:
/config-optimize-settings
```
### Applied Output
```
+-----------------------------------------------------------------+
| CONFIG-MAINTAINER - Settings Optimization |
+-----------------------------------------------------------------+
Optimization Applied Successfully
Backup: .claude/backups/settings.local.json.20260202-143022
[... summary of changes ...]
Score: 67/100 → 85/100
```
## DO NOT
- Apply changes without showing preview
- Remove deny rules silently
- Add unscoped `Bash` permission
- Skip backup without explicit `--no-backup` flag
- Apply `autonomous` profile without sandbox confirmation
- Recommend broad permissions without verifying review layers

View File

@@ -1,256 +0,0 @@
---
name: config-permissions-map
description: Generate visual map of review layers and permission coverage
---
# /config-permissions-map
Generate a Mermaid diagram showing the relationship between file operations, review layers, and permission status.
## Skills to Load
Before executing, load:
- `skills/visual-header.md`
- `skills/settings-optimization.md`
Also read: `/mnt/skills/user/mermaid-diagrams/SKILL.md` (for diagram requirements)
## Visual Output
```
+-----------------------------------------------------------------+
| CONFIG-MAINTAINER - Permissions Map |
+-----------------------------------------------------------------+
```
## Usage
```
/config-permissions-map # Generate and display diagram
/config-permissions-map --save # Save diagram to .mermaid file
```
## Workflow
### Step 1: Detect Active Hooks
Read all plugin hooks from the marketplace:
```
plugins/code-sentinel/hooks/hooks.json
plugins/doc-guardian/hooks/hooks.json
plugins/project-hygiene/hooks/hooks.json
plugins/data-platform/hooks/hooks.json
plugins/contract-validator/hooks/hooks.json
plugins/cmdb-assistant/hooks/hooks.json
```
For each hook, extract:
- Event type (PreToolUse, PostToolUse, SessionStart, etc.)
- Tool matchers (Write, Edit, MultiEdit, Bash patterns)
- Hook command/script
### Step 2: Map Hooks to File Scopes
Create a mapping of which review layers cover which operations:
| Operation | PreToolUse Hooks | PostToolUse Hooks | Other Gates |
|-----------|------------------|-------------------|-------------|
| Write | code-sentinel | doc-guardian, project-hygiene | PR review |
| Edit | code-sentinel | doc-guardian, project-hygiene | PR review |
| MultiEdit | code-sentinel | doc-guardian | PR review |
| Bash(git *) | git-flow | — | — |
### Step 3: Read Current Permissions
Load `.claude/settings.local.json` and parse:
- `allow` array → auto-allowed operations
- `deny` array → blocked operations
- `ask` array → always-prompted operations
### Step 4: Generate Mermaid Flowchart
**Diagram requirements (from mermaid-diagrams skill):**
- Use `classDef` for styling
- Maximum 3 colors (blue, green, amber/purple)
- Semantic arrow labels
- Left-to-right flow
**Structure:**
```mermaid
flowchart LR
subgraph ops[File Operations]
direction TB
W[Write]
E[Edit]
ME[MultiEdit]
BG[Bash git]
BN[Bash npm]
BO[Bash other]
end
subgraph pre[PreToolUse Hooks]
direction TB
CS[code-sentinel<br/>Security Scan]
GF[git-flow<br/>Branch Check]
end
subgraph post[PostToolUse Hooks]
direction TB
DG[doc-guardian<br/>Drift Detection]
PH[project-hygiene<br/>Cleanup]
DP[data-platform<br/>Schema Diff]
end
subgraph perm[Permission Status]
direction TB
AA[Auto-Allowed]
PR[Prompted]
DN[Denied]
end
W -->|intercepted| CS
W -->|tracked| DG
E -->|intercepted| CS
E -->|tracked| DG
BG -->|checked| GF
CS -->|passed| AA
DG -->|logged| AA
GF -->|valid| AA
BO -->|no hook| PR
classDef preHook fill:#e3f2fd,stroke:#1976d2
classDef postHook fill:#e8f5e9,stroke:#388e3c
classDef sprint fill:#fff3e0,stroke:#f57c00
classDef prReview fill:#f3e5f5,stroke:#7b1fa2
classDef allowed fill:#c8e6c9,stroke:#2e7d32
classDef prompted fill:#fff9c4,stroke:#f9a825
classDef denied fill:#ffcdd2,stroke:#c62828
class CS,GF preHook
class DG,PH,DP postHook
class AA allowed
class PR prompted
class DN denied
```
### Step 5: Generate Coverage Summary Table
```
Review Layer Coverage Summary
=============================
| Directory Scope | Layers | Status | Recommendation |
|--------------------------|--------|-----------------|----------------|
| plugins/*/commands/*.md | 3 | ✓ Auto-allowed | — |
| plugins/*/skills/*.md | 2 | ✓ Auto-allowed | — |
| mcp-servers/**/*.py | 3 | ✓ Auto-allowed | — |
| docs/** | 2 | ✓ Auto-allowed | — |
| scripts/*.sh | 2 | ⚠ Prompted | Consider auto-allow |
| .env* | 0 | ✗ Denied | Correct - secrets |
| Root directory | 1 | ⚠ Prompted | Keep prompted |
Legend:
✓ = Covered by ≥2 review layers, auto-allowed
⚠ = Fewer than 2 layers or not allowed
✗ = Explicitly denied
```
### Step 6: Identify Gaps
Report any gaps in coverage:
```
Coverage Gaps Detected:
1. Bash(npm *) — not in allow list, but npm operations are common
→ 2 review layers active, could be auto-allowed
2. mcp__data-platform__* — MCP server configured but tools not allowed
→ Add to allow list to avoid prompts
3. scripts/*.sh — 2 review layers but still prompted
→ Consider adding Write(scripts/**) to allow
```
### Step 7: Output Diagram
Display the Mermaid diagram inline.
If `--save` flag is used:
- Save to `.claude/permissions-map.mermaid`
- Report the file path
## Output Format
```
+-----------------------------------------------------------------+
| CONFIG-MAINTAINER - Permissions Map |
+-----------------------------------------------------------------+
Review Layer Status
===================
PreToolUse Hooks (intercept before operation):
✓ code-sentinel — Write, Edit, MultiEdit
✓ git-flow — Bash(git checkout *), Bash(git commit *)
PostToolUse Hooks (track after operation):
✓ doc-guardian — Write, Edit, MultiEdit
✓ project-hygiene — Write, Edit
✗ data-platform — not detected
Other Review Gates:
✓ Sprint Approval (projman milestone workflow)
✓ PR Review (pr-review multi-agent)
Permissions Flow Diagram
========================
```mermaid
[diagram here]
```
Coverage Summary
================
[table here]
Gaps & Recommendations
======================
[gaps list here]
```
## File Output (--save flag)
When `--save` is specified:
```
Diagram saved to: .claude/permissions-map.mermaid
To view:
- Open in VS Code with Mermaid extension
- Paste into https://mermaid.live
- Include in documentation with ```mermaid code fence
```
## Color Scheme
| Element | Color | Hex |
|---------|-------|-----|
| PreToolUse hooks | Blue | #e3f2fd |
| PostToolUse hooks | Green | #e8f5e9 |
| Sprint/Planning gates | Amber | #fff3e0 |
| PR Review | Purple | #f3e5f5 |
| Auto-allowed | Light green | #c8e6c9 |
| Prompted | Light yellow | #fff9c4 |
| Denied | Light red | #ffcdd2 |
## DO NOT
- Generate diagrams without reading the mermaid-diagrams skill
- Use more than 3 primary colors in the diagram
- Skip the coverage summary table
- Fail to identify coverage gaps

View File

@@ -4,46 +4,208 @@ description: Initialize a new CLAUDE.md file for a project
# Initialize CLAUDE.md
Create a new CLAUDE.md file tailored to your project.
This command creates a new CLAUDE.md file tailored to your project, gathering context and generating appropriate content.
## Skills to Load
## What This Command Does
- skills/visual-header.md
- skills/claude-md-structure.md
- skills/pre-change-protocol.md
## Visual Output
Display: `CONFIG-MAINTAINER - CLAUDE.md Initialization`
1. **Gather Context** - Analyzes project structure and asks clarifying questions
2. **Detect Stack** - Identifies technologies, frameworks, and tools
3. **Generate Content** - Creates tailored CLAUDE.md sections
4. **Review & Refine** - Allows customization before saving
5. **Save File** - Creates the CLAUDE.md in project root
## Usage
```
/config-init # Interactive
/config-init --minimal # Minimal version
/config-init --comprehensive # Detailed version
/config-init
```
## Workflow
Or with options:
1. Analyze project structure, ask clarifying questions
2. Detect technologies, frameworks, tools
3. Generate tailored CLAUDE.md sections
4. Allow review and customization
5. Save file in project root
```
/config-init --template=api # Use API project template
/config-init --minimal # Create minimal version
/config-init --comprehensive # Create detailed version
```
## Initialization Workflow
```
CLAUDE.md Initialization
========================
Step 1: Project Analysis
------------------------
Scanning project structure...
Detected:
- Language: Python 3.11
- Framework: FastAPI
- Package Manager: pip (requirements.txt found)
- Testing: pytest
- Docker: Yes (Dockerfile found)
- Git: Yes (.git directory)
Step 2: Clarifying Questions
----------------------------
1. Project Description:
What does this project do? (1-2 sentences)
> [User provides description]
2. Build/Run Commands:
Detected commands - are these correct?
- Install: pip install -r requirements.txt
- Test: pytest
- Run: uvicorn main:app --reload
[Y/n/edit]
3. Critical Rules:
Any rules Claude MUST follow?
Examples: "Never modify migrations", "Always use type hints"
> [User provides rules]
4. Sensitive Areas:
Any files/directories Claude should be careful with?
> [User provides or skips]
Step 3: Generate CLAUDE.md
--------------------------
Generating content based on:
- Project type: FastAPI web API
- Detected technologies
- Your provided context
Preview:
---
# CLAUDE.md
## Project Overview
[Generated description]
## Quick Start
```bash
pip install -r requirements.txt # Install dependencies
pytest # Run tests
uvicorn main:app --reload # Start dev server
```
## Architecture
[Generated based on structure]
## Critical Rules
[Your provided rules]
## File Structure
[Generated from analysis]
---
Save this CLAUDE.md? [Y/n/edit]
Step 4: Complete
----------------
CLAUDE.md created successfully!
Location: /path/to/project/CLAUDE.md
Lines: 87
Score: 85/100 (following best practices)
Recommendations:
- Run /config-analyze periodically to maintain quality
- Update when adding major features
- Add troubleshooting section as issues are discovered
```
## Templates
| Template | Sections |
|----------|----------|
| Minimal | Overview, Quick Start, Critical Rules, Pre-Change Protocol |
| Standard | + Architecture, Common Operations, File Structure |
| Comprehensive | + Troubleshooting, Integration Points, Workflow |
### Minimal Template
For small projects or when starting fresh:
- Project Overview (required)
- Quick Start (required)
- Critical Rules (required)
**Note:** Pre-Change Protocol is MANDATORY in all templates.
### Standard Template (default)
For typical projects:
- Project Overview
- Quick Start
- Architecture
- Critical Rules
- Common Operations
- File Structure
### Comprehensive Template
For large or complex projects:
- All standard sections plus:
- Detailed Architecture
- Troubleshooting
- Integration Points
- Development Workflow
- Deployment Notes
## Auto-Detection
The command automatically detects:
| What | How |
|------|-----|
| Language | File extensions, config files |
| Framework | package.json, requirements.txt, etc. |
| Build system | Makefile, package.json scripts, etc. |
| Testing | pytest.ini, jest.config, etc. |
| Docker | Dockerfile, docker-compose.yml |
| Database | Connection strings, ORM configs |
## Customization
After generation, you can:
- Edit any section before saving
- Add additional sections
- Remove unnecessary sections
- Adjust detail level
- Add project-specific content
## When to Use
Run `/config-init` when:
- Starting a new project
- Project lacks CLAUDE.md
- Taking over unfamiliar project
- Existing CLAUDE.md is outdated/poor quality
- Taking over an unfamiliar project
## Tips
1. **Provide accurate description** - This shapes the whole file
2. **Include critical rules** - What must Claude never do?
3. **Review generated content** - Auto-detection isn't perfect
4. **Start minimal, grow as needed** - Add sections when required
5. **Keep it current** - Update when project changes significantly
## Examples
### For a CLI Tool
```
/config-init
> Description: CLI tool for managing cloud infrastructure
> Critical rules: Never delete resources without confirmation, always show dry-run first
```
### For a Web App
```
/config-init
> Description: E-commerce platform with React frontend and Node.js backend
> Critical rules: Never expose API keys, always validate user input, follow the existing component patterns
```
### For a Library
```
/config-init --template=minimal
> Description: Python library for parsing log files
> Critical rules: Maintain backward compatibility, all public functions need docstrings
```

View File

@@ -4,47 +4,175 @@ description: Optimize CLAUDE.md structure and content
# Optimize CLAUDE.md
Automatically optimize CLAUDE.md based on best practices.
This command automatically optimizes your project's CLAUDE.md file based on best practices and identified issues.
## Skills to Load
## What This Command Does
- skills/visual-header.md
- skills/optimization-patterns.md
- skills/pre-change-protocol.md
- skills/claude-md-structure.md
## Visual Output
Display: `CONFIG-MAINTAINER - CLAUDE.md Optimization`
1. **Analyze Current File** - Identifies all optimization opportunities
2. **Plan Changes** - Determines what to restructure, condense, or add
3. **Show Preview** - Displays before/after comparison
4. **Apply Changes** - Updates the file with your approval
5. **Verify Results** - Confirms improvements achieved
## Usage
```
/config-optimize # Full optimization
/config-optimize --condense # Reduce verbosity
/config-optimize --dry-run # Preview only
/config-optimize
```
## Workflow
Or specify specific optimizations:
1. Identify optimization opportunities
2. Plan restructure, condense, or add actions
3. Show before/after preview
4. Apply changes with approval
5. Verify improvements
```
/config-optimize --condense # Focus on reducing verbosity
/config-optimize --restructure # Focus on reorganization
/config-optimize --add-missing # Focus on adding missing sections
```
## Optimization Actions
### Restructure
- Reorder sections by importance
- Group related content together
- Improve header hierarchy
- Add navigation aids
### Condense
- Remove redundant explanations
- Convert verbose text to bullet points
- Eliminate duplicate content
- Shorten overly detailed sections
### Enhance
- Add missing essential sections
- Improve unclear instructions
- Add helpful examples
- Highlight critical rules
### Format
- Standardize header styles
- Fix code block formatting
- Align list formatting
- Improve table layouts
## Expected Output
```
CLAUDE.md Optimization
======================
Current Analysis:
- Score: 72/100
- Lines: 245
- Issues: 4
Planned Optimizations:
1. ADD: Quick Start section (new, ~15 lines)
+ Build command
+ Test command
+ Run command
2. CONDENSE: Testing section (34 → 8 lines)
Before: Verbose explanation with redundant setup info
After: Concise command reference with comments
3. REMOVE: Duplicate git workflow (lines 189-200)
Keeping: Original at lines 102-115
4. FORMAT: Standardize headers
Changing 12 headers from "## Title:" to "## Title"
Preview Changes? [Y/n] y
--- CLAUDE.md (before)
+++ CLAUDE.md (after)
@@ -1,5 +1,20 @@
# CLAUDE.md
+## Quick Start
+
+```bash
+# Install dependencies
+pip install -r requirements.txt
+
+# Run tests
+pytest
+
+# Start development server
+python manage.py runserver
+```
+
## Project Overview
...
[Full diff shown]
Apply these changes? [Y/n] y
Optimization Complete!
- Previous score: 72/100
- New score: 89/100
- Lines reduced: 245 → 198 (-19%)
- Issues resolved: 4/4
Backup saved to: .claude/backups/CLAUDE.md.2025-01-18
```
## Safety Features
### Backup Creation
- Automatic backup before changes
- Stored in `.claude/backups/`
- Easy restoration if needed
### Preview Mode
- All changes shown before applying
- Diff format for easy review
- Option to approve/reject
### Selective Application
- Can apply individual changes
- Skip specific optimizations
- Iterative refinement
## Options
| Option | Description |
|--------|-------------|
| `--dry-run` | Preview without applying |
| `--no-backup` | Skip backup |
| `--dry-run` | Show changes without applying |
| `--no-backup` | Skip backup creation |
| `--aggressive` | Maximum condensation |
| `--section=NAME` | Optimize specific section |
| `--preserve-comments` | Keep all existing comments |
| `--section=NAME` | Optimize specific section only |
**Priority:** Add Pre-Change Protocol if missing.
## When to Use
## Safety
Run `/config-optimize` when:
- Analysis shows score below 70
- File has grown too long
- Structure needs reorganization
- Missing critical sections
- After major refactoring
- Auto backup to `.claude/backups/`
- Preview before applying
## Best Practices
1. **Run analysis first** - Understand current state
2. **Review preview carefully** - Ensure nothing important lost
3. **Test after changes** - Verify Claude follows instructions
4. **Keep backups** - Restore if issues arise
5. **Iterate** - Multiple small optimizations beat one large one
## Rollback
If optimization causes issues:
```bash
# Restore from backup
cp .claude/backups/CLAUDE.md.TIMESTAMP ./CLAUDE.md
```
Or ask:
```
Restore CLAUDE.md from the most recent backup
```

View File

@@ -2,13 +2,8 @@
"hooks": {
"SessionStart": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/enforce-rules.sh"
}
]
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/enforce-rules.sh"
}
]
}

View File

@@ -1,112 +0,0 @@
# CLAUDE.md Analysis Workflow
This skill defines the workflow for analyzing CLAUDE.md files.
## Analysis Steps
1. **Locate File** - Find CLAUDE.md in project root
2. **Parse Structure** - Extract headers and sections
3. **Evaluate Content** - Score against criteria
4. **Detect Plugins** - Identify active marketplace plugins
5. **Check Integration** - Verify plugin references
6. **Generate Report** - Provide scored assessment
## Content Analysis
### What to Check
| Area | Check For |
|------|-----------|
| Structure | Header hierarchy, section ordering, grouping |
| Clarity | Clear instructions, examples, unambiguous language |
| Completeness | Required sections present, workflows documented |
| Conciseness | No redundancy, efficient density, appropriate length |
### Required Sections Check
1. Project Overview - present?
2. Quick Start - present with commands?
3. Critical Rules - present?
4. **Pre-Change Protocol** - present? (HIGH PRIORITY if missing)
## Plugin Integration Analysis
### Detection Method
1. Read `.claude/settings.local.json` for enabled MCP servers
2. Map MCP servers to plugins:
- `gitea` -> projman
- `netbox` -> cmdb-assistant
3. Check for hook-based plugins (project-hygiene)
4. Scan CLAUDE.md for plugin references
### Coverage Scoring
For each detected plugin, verify CLAUDE.md contains:
- Plugin section header or mention
- Available commands documentation
- MCP tools reference (if applicable)
- Usage guidelines
Coverage = (plugins referenced / plugins detected) * 100%
## Report Format
```
CLAUDE.md Analysis Report
=========================
File: /path/to/project/CLAUDE.md
Lines: N
Last Modified: YYYY-MM-DD
Overall Score: NN/100
Category Scores:
- Structure: NN/25 (Rating)
- Clarity: NN/25 (Rating)
- Completeness: NN/25 (Rating)
- Conciseness: NN/25 (Rating)
Strengths:
+ [Positive finding]
Issues Found:
N. [SEVERITY] Issue description (location)
Context explaining the problem.
Impact: What happens if not fixed.
Recommendations:
N. Action to take (priority: high/medium/low)
---
Plugin Integration Analysis
===========================
Detected Active Plugins:
[check] plugin-name (via detection method)
Plugin Coverage: NN% (N/N plugins referenced)
Missing Integration Content:
N. plugin-name
What to add.
```
## Issue Severity
| Level | When to Use |
|-------|-------------|
| HIGH | Missing mandatory sections, security issues |
| MEDIUM | Missing recommended content, duplicate content |
| LOW | Formatting issues, minor improvements |
## Follow-Up Actions
After analysis, offer:
1. Implement all content recommendations
2. Add missing plugin integrations
3. Do both (recommended)
4. Show preview of changes first

Some files were not shown because too many files have changed in this diff Show More