Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,26 @@
},
"metadata": {
"description": "PSD Plugin Marketplace for Claude Code and Claude Cowork — coding workflows, productivity agents, and district automation",
"version": "2.22.1",
"version": "2.23.0",
"pluginRoot": "./plugins"
},
"plugins": [
{
"name": "psd-infrastructure",
"source": "./plugins/psd-infrastructure",
"description": "PSD network and systems infrastructure — Aruba wireless, FortiAnalyzer log search, Freshservice tickets, and DocBot documentation as MCP servers with companion skills; server code auto-updates from psd401 repos on launch",
"category": "infrastructure",
"keywords": [
"infrastructure",
"networking",
"aruba",
"fortianalyzer",
"freshservice",
"documentation",
"psd",
"education"
]
},
{
"name": "psd-coding-system",
"source": "./plugins/psd-coding-system",
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **New plugin: `psd-infrastructure`** — network and systems tooling as four bundled MCP servers with companion skills: Aruba wireless (`aruba-wireless`, 6 tools), FortiAnalyzer (`fortianalyzer-logs`, 25 tools), Freshservice (`freshservice-tickets`, 15 tools), DocBot (`docbot-docs`, 30 tools). Server code lives in internal psd401 repos ([aruba-mcp](https://github.com/psd401/aruba-mcp), [fortianalyzer-mcp](https://github.com/psd401/fortianalyzer-mcp), [freshservice-mcp](https://github.com/psd401/freshservice-mcp), [DocBot](https://github.com/psd401/DocBot)) and is cloned/`git pull`ed by the launcher on every server start, so server-side changes ship to users without a plugin release. The plugin deliberately omits the `version` field so every marketplace commit counts as a new plugin version (no forgotten-bump staleness). Credentials via env vars or `~/.config/psd-infrastructure/.env` (see the plugin's SECRETS-SETUP.md); requires `gh`, `bun`, `uv`. Marketplace **2.22.1 → 2.23.0**.
- **Cloud routines for triage, lfg, and pr-fix** (no plugin version bump — routines are infrastructure, not part of `/plugin install` distribution):
- **`routines/triage`** — autonomous FreshService → GitHub triage. Runs twice daily (cron `0 6,18 * * *`). Polls software-dev workspace (ID 13), filters by `[claude-routine-triaged]` private-note marker, classifies tickets to `psd401/aistudio` / `psd401/psd-workflow-automation` / `psd401/psd-claude-plugins`, runs full Phase 1.5 diagnosis fan-out (`repo-research-analyst` + `git-history-analyzer` + `bug-reproduction-validator`), files an issue, posts private note (with diagnosis brief) + public reply to FreshService. Per-fire cap of 5 tickets.
- **`routines/lfg`** — autonomous end-to-end implementation for issues labeled `lfg-ready`. Runs every 6 hours (cron `0 */6 * * *`). Label state machine: `lfg-ready` → `lfg-in-progress` → `lfg-pr-open` / `lfg-blocked`. Branches `claude/lfg-issue-<N>-<slug>` from `dev` (PSD convention), runs research + implement + test + validate + security-audit, opens PR targeting `dev`. One issue per fire. Honors `lfg-skip` opt-out label.
Expand Down
48 changes: 48 additions & 0 deletions plugins/psd-infrastructure/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"$schema": "https://cdn.jsdelivr.net/npm/@anthropic-ai/claude-code@latest/plugin.schema.json",
"name": "psd-infrastructure",
"description": "PSD network and systems infrastructure tools — Aruba wireless, FortiAnalyzer log search, Freshservice tickets, and DocBot documentation, as MCP servers with companion skills. Server code auto-updates from psd401 GitHub repos on launch.",
"author": {
"name": "Reese Herber",
"email": "herberr@psd401.net",
"organization": "Peninsula School District"
},
"repository": "https://github.com/psd401/psd-claude-plugins",
"keywords": [
"infrastructure",
"networking",
"aruba",
"fortianalyzer",
"freshservice",
"documentation",
"psd",
"education"
],
"license": "MIT",
"mcpServers": {
"aruba": {
"type": "stdio",
"command": "bash",
"args": ["${CLAUDE_PLUGIN_ROOT}/scripts/run-server.sh", "aruba"],
"env": { "PSD_INFRA_HOME": "${CLAUDE_PLUGIN_DATA}" }
},
"fortianalyzer": {
"type": "stdio",
"command": "bash",
"args": ["${CLAUDE_PLUGIN_ROOT}/scripts/run-server.sh", "fortianalyzer"],
"env": { "PSD_INFRA_HOME": "${CLAUDE_PLUGIN_DATA}" }
},
"freshservice": {
"type": "stdio",
"command": "bash",
"args": ["${CLAUDE_PLUGIN_ROOT}/scripts/run-server.sh", "freshservice"],
"env": { "PSD_INFRA_HOME": "${CLAUDE_PLUGIN_DATA}" }
},
"docbot": {
"type": "stdio",
"command": "bash",
"args": ["${CLAUDE_PLUGIN_ROOT}/scripts/run-server.sh", "docbot"],
"env": { "PSD_INFRA_HOME": "${CLAUDE_PLUGIN_DATA}" }
}
}
}
84 changes: 84 additions & 0 deletions plugins/psd-infrastructure/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# PSD Infrastructure

Network and systems tooling for Peninsula School District IT, packaged as MCP
servers plus companion skills that teach Claude the workflows and pitfalls for
each system.

## What's included

| System | MCP server | Skill | Tools |
|---|---|---|---|
| Aruba wireless controllers | `aruba` | `aruba-wireless` | 6 (APs, clients, config, logs — read-only) |
| FortiAnalyzer | `fortianalyzer` | `fortianalyzer-logs` | 25 (log search, alerts, reports, devices) |
| Freshservice | `freshservice` | `freshservice-tickets` | 15 (tickets, replies, context, KB, similar-ticket matching) |
| DocBot (BookStack + FS docs) | `docbot` | `docbot-docs` | 30 (audits, propose/approve/apply changes, screenshots) |

Server source lives in internal psd401 repos:
[aruba-mcp](https://github.com/psd401/aruba-mcp),
[fortianalyzer-mcp](https://github.com/psd401/fortianalyzer-mcp),
[freshservice-mcp](https://github.com/psd401/freshservice-mcp),
[DocBot](https://github.com/psd401/DocBot).

## Install (one time)

```
/plugin marketplace add psd401/psd-claude-plugins # skip if already added
/plugin install psd-infrastructure@psd-claude-plugins
```

Then:

1. **Enable auto-update** for the marketplace: `/plugin` → Marketplaces tab →
psd-claude-plugins → Enable auto-update. Third-party marketplaces ship with
auto-update off, so without this step you only get updates by running
`/plugin marketplace update psd-claude-plugins` manually.
2. **Set up credentials**: see [SECRETS-SETUP.md](./SECRETS-SETUP.md).
Requires `gh` (authenticated), `bun`, and `uv` — install commands are in
that file too.

## How updates work

Two layers update independently, and both are automatic after the one-time
setup above:

1. **Server code.** On every launch, the launcher (`scripts/run-server.sh`)
does a fast-forward `git pull` of the server repo before starting it. Push
to a server repo's default branch and everyone runs the new code on their
next Claude Code session. Offline or failed pulls fall back to the cached
copy (kept under Claude's plugin data dir, surviving plugin updates).
2. **Skills and plugin config.** With marketplace auto-update enabled, Claude
Code checks for updates shortly after each session starts and either
prompts `/reload-plugins` or applies them on the next launch. This plugin
deliberately has **no `version` field**, so for git-based installs every
pushed commit counts as a new version — no version bump needed, and no
forgotten-bump silent staleness.

Team repos can auto-prompt installation for everyone who trusts the folder by
committing this to the repo's `.claude/settings.json`:

```json
{
"extraKnownMarketplaces": {
"psd-claude-plugins": {
"source": { "source": "github", "repo": "psd401/psd-claude-plugins" }
}
},
"enabledPlugins": {
"psd-infrastructure@psd-claude-plugins": true
}
}
```

Org-wide, managed settings can additionally set `"autoUpdate": true` on the
marketplace entry so nobody has to flip the toggle themselves.

## First launch is slow

The first session after install clones each server repo and installs its
dependencies (a few minutes total). Later launches start in seconds.

## Not included (yet)

`zabbix` and `onesync` MCP servers exist but are excluded from this plugin
until connection issues are resolved. The `mac-mini` bridge and `pinchtab`
browser servers are personal tooling and stay out of the shared plugin.
63 changes: 63 additions & 0 deletions plugins/psd-infrastructure/SECRETS-SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Secrets Setup for PSD Infrastructure

Every server in this plugin talks to a real PSD system and needs credentials.
Nothing works until you set these up. Credentials never live in this repo.

## Prerequisites

- `gh` CLI authenticated to GitHub (`gh auth login`) — the server code lives in
internal psd401 repos and is cloned on first launch
- `bun` (`brew install oven-sh/bun/bun`) — runs fortianalyzer, freshservice, docbot
- `uv` (`brew install uv`) — runs the Aruba server

## Which servers need which keys?

| Server | Required | Optional |
|---|---|---|
| `aruba` | `ARUBA_HOST`, `ARUBA_USERNAME`, `ARUBA_PASSWORD` | `ARUBA_PORT` (default 4343), `ARUBA_VERIFY_SSL` (default false) |
| `fortianalyzer` | `FAZ_HOST`, `FAZ_USERNAME`, `FAZ_PASSWORD` | `FAZ_ADOM` |
| `freshservice` | `FRESHSERVICE_DOMAIN`, `FRESHSERVICE_API_KEY`, `FRESHSERVICE_AGENT_EMAIL` | |
| `docbot` | `BOOKSTACK_URL`, `BOOKSTACK_TOKEN_ID`, `BOOKSTACK_TOKEN_SECRET` | `FRESHSERVICE_ENABLED`, `FRESHSERVICE_DOMAIN`, `FRESHSERVICE_API_KEY`, `GEMINI_API_KEY`, `DOCBOT_DB_PATH`, `DOCBOT_LOG_LEVEL` |

Ask Reese Herber (herberr@psd401.net) for host values and service accounts.
Freshservice API keys are per-agent: Freshservice → Profile Settings → API Key.

## Option A: Shared env file (recommended)

Create `~/.config/psd-infrastructure/.env`:

```bash
mkdir -p ~/.config/psd-infrastructure
cat > ~/.config/psd-infrastructure/.env <<'EOF'
ARUBA_HOST=...
ARUBA_USERNAME=...
ARUBA_PASSWORD=...

FAZ_HOST=...
FAZ_USERNAME=...
FAZ_PASSWORD=...

FRESHSERVICE_DOMAIN=...
FRESHSERVICE_API_KEY=...
FRESHSERVICE_AGENT_EMAIL=you@psd401.net

BOOKSTACK_URL=...
BOOKSTACK_TOKEN_ID=...
BOOKSTACK_TOKEN_SECRET=...
EOF
chmod 600 ~/.config/psd-infrastructure/.env
```

The launcher script loads this file automatically before starting each server.
Set `PSD_INFRA_ENV` to point somewhere else if you keep secrets elsewhere.

## Option B: Shell profile

Export the same variables from `~/.zshrc`. Both options work; the env file
keeps infrastructure credentials separate from your general shell environment.

## Only using some servers?

Set up only the credentials you need. A server with missing credentials fails
on first tool call with a clear error; the others are unaffected. You can also
disable individual servers under `/plugin` → manage plugins.
71 changes: 71 additions & 0 deletions plugins/psd-infrastructure/scripts/run-server.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# Launcher for psd-infrastructure MCP servers.
# Clones the server repo on first use, pulls updates on every launch,
# then execs the server. Server code lives in internal psd401 repos;
# users need `gh auth login` (or equivalent git credentials) once.
set -euo pipefail

NAME="${1:?usage: run-server.sh <aruba|fortianalyzer|freshservice|docbot>}"
CACHE_ROOT="${PSD_INFRA_HOME:-$HOME/.psd-infrastructure}/servers"
DIR="$CACHE_ROOT/$NAME"

case "$NAME" in
aruba) REPO="psd401/aruba-mcp" ;;
fortianalyzer) REPO="psd401/fortianalyzer-mcp" ;;
freshservice) REPO="psd401/freshservice-mcp" ;;
docbot) REPO="psd401/DocBot" ;;
*) echo "unknown server: $NAME" >&2; exit 1 ;;
esac

# Optional shared env file (see SECRETS-SETUP.md). Shell-profile exports also work.
ENV_FILE="${PSD_INFRA_ENV:-$HOME/.config/psd-infrastructure/.env}"
if [ -f "$ENV_FILE" ]; then
set -a
# shellcheck disable=SC1090
. "$ENV_FILE"
set +a
fi

log() { echo "[psd-infrastructure/$NAME] $*" >&2; }

clone_repo() {
mkdir -p "$CACHE_ROOT"
if command -v gh >/dev/null 2>&1; then
gh repo clone "$REPO" "$DIR" -- --quiet >&2
else
git clone --quiet "https://github.com/$REPO.git" "$DIR" >&2
fi
}
Comment on lines +31 to +38

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If gh is installed but not authenticated (or authenticated to a different account/org), gh repo clone will fail and immediately exit the script due to set -e. Adding a fallback to standard git clone on failure, and cleaning up any partial directory, makes the cloning process much more robust.

Suggested change
clone_repo() {
mkdir -p "$CACHE_ROOT"
if command -v gh >/dev/null 2>&1; then
gh repo clone "$REPO" "$DIR" -- --quiet >&2
else
git clone --quiet "https://github.com/$REPO.git" "$DIR" >&2
fi
}
clone_repo() {
mkdir -p "$CACHE_ROOT"
if command -v gh >/dev/null 2>&1; then
gh repo clone "$REPO" "$DIR" -- --quiet >&2 || {
log "gh repo clone failed, falling back to git clone"
rm -rf "$DIR"
git clone --quiet "https://github.com/$REPO.git" "$DIR" >&2
}
else
git clone --quiet "https://github.com/$REPO.git" "$DIR" >&2
fi
}


if [ ! -d "$DIR/.git" ]; then
log "first run: cloning $REPO"
clone_repo
else
# Best-effort update; a failed pull (offline, etc.) falls back to cached code.
git -C "$DIR" pull --ff-only --quiet >&2 || log "update check failed, using cached copy"
fi

cd "$DIR"

case "$NAME" in
aruba)
command -v uv >/dev/null 2>&1 || { log "uv is required (brew install uv)"; exit 1; }
exec uv run aruba_mcp.py
;;
fortianalyzer)
command -v bun >/dev/null 2>&1 || { log "bun is required (brew install oven-sh/bun/bun)"; exit 1; }
bun install --silent >&2 || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Running bun install on every single launch of the MCP server adds unnecessary startup latency and network overhead. We can optimize this by only running bun install if node_modules does not exist or if package.json is newer than node_modules (indicating an update occurred). We only touch node_modules on success to ensure retries on failure.

Suggested change
bun install --silent >&2 || true
if [ ! -d "node_modules" ] || [ "package.json" -nt "node_modules" ]; then
if bun install --silent >&2; then
touch "node_modules"
fi
fi

exec bun run src/index.ts
;;
freshservice)
command -v bun >/dev/null 2>&1 || { log "bun is required (brew install oven-sh/bun/bun)"; exit 1; }
bun install --silent >&2 || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Running bun install on every single launch of the MCP server adds unnecessary startup latency and network overhead. We can optimize this by only running bun install if node_modules does not exist or if package.json is newer than node_modules (indicating an update occurred). We only touch node_modules on success to ensure retries on failure.

Suggested change
bun install --silent >&2 || true
if [ ! -d "node_modules" ] || [ "package.json" -nt "node_modules" ]; then
if bun install --silent >&2; then
touch "node_modules"
fi
fi

exec bun run src/index.ts
;;
docbot)
command -v bun >/dev/null 2>&1 || { log "bun is required (brew install oven-sh/bun/bun)"; exit 1; }
bun install --silent >&2 || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Running bun install on every single launch of the MCP server adds unnecessary startup latency and network overhead. We can optimize this by only running bun install if node_modules does not exist or if package.json is newer than node_modules (indicating an update occurred). We only touch node_modules on success to ensure retries on failure.

Suggested change
bun install --silent >&2 || true
if [ ! -d "node_modules" ] || [ "package.json" -nt "node_modules" ]; then
if bun install --silent >&2; then
touch "node_modules"
fi
fi

export DOCBOT_DB_PATH="${DOCBOT_DB_PATH:-$DIR/docbot.db}"
exec bun run src/index.ts
;;
esac
60 changes: 60 additions & 0 deletions plugins/psd-infrastructure/skills/aruba-wireless/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
name: aruba-wireless
description: Query the Aruba wireless Mobility Conductor for AP status, connected clients, running config, system logs, and wireless configuration objects (SSID/VAP/AP-group/role) for read-only network troubleshooting; use when an AP is down, a user can't connect to wifi, or you need to review wireless configuration.
triggers:
- "check aruba"
- "AP is down"
- "wireless AP offline"
- "check wifi clients"
- "who's connected to this SSID"
- "check wireless config"
- "aruba controller status"
- "check AP group"
- "wireless troubleshooting"
allowed-tools: Read, Bash
version: 0.1.0
Comment on lines +14 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Specifying allowed-tools: Read, Bash in the skill frontmatter restricts Claude to only using those two tools when this skill is active. This will completely block Claude from calling any of the custom aruba_* MCP tools defined by the plugin. Removing this restriction allows Claude to use all registered tools, including the MCP tools.

Suggested change
allowed-tools: Read, Bash
version: 0.1.0
version: 0.1.0

---

# Aruba Wireless Skill

## What this covers

The `aruba` MCP server talks to PSD's Aruba Mobility Conductor for read-only wireless monitoring and config review: AP status, connected clients, running config, system logs, and AOS 8 hierarchical configuration objects (SSID/VAP/AP-group/role). Use it for wireless troubleshooting and config auditing — there is no tool to change config, reboot an AP, or kick a client. All queries run against the conductor; it does not reliably reach through to individual managed devices (see Pitfalls).

## Available tools

| Tool | Purpose |
|---|---|
| `aruba_show` | Run any `show ...` AOS CLI command and return the JSON result |
| `aruba_get_aps` | Query the AP database, optionally filtered by group name or up/down status |
| `aruba_get_clients` | Query connected clients/users, optionally filtered by SSID substring |
| `aruba_get_config` | Retrieve running-config, full or a single section by keyword |
| `aruba_get_logs` | Retrieve system logs by category (system/security/wireless/etc), default 50 entries |
| `aruba_get_node_config` | Query AOS 8 hierarchical configuration objects (SSID/VAP/AP-group/role) for a specific config path |

## Common workflows

1. **AP is reported down.** `aruba_get_aps` with `status="down"` (optionally `group="<group-name>"`) to confirm and scope the outage → `aruba_get_logs` with `category="wireless"` to check for recent flap/reboot events around that time → `aruba_show` with a targeted command like `show ap debug system-status ap-name <ap-name>` for detail on that specific AP.

2. **User can't connect to a specific SSID.** `aruba_get_clients` with `network="<ssid-substring>"` to see who's currently associated on that network → `aruba_get_logs` with `category="security"` to check for recent auth rejects → `aruba_show` with `command="show aaa authentication dot1x default"` (or the relevant dot1x profile) to check EAP/termination settings.

3. **Review a group's wireless profile configuration.** `aruba_get_aps` with `group="<group-name>"` to confirm the AP group exists and see its membership → `aruba_get_node_config` with `config_path="/md/<group-name>"` and one specific `object_type` (e.g. `"ssid_prof"`) to pull just that profile type for the group. Never omit `object_type` or loop this across every object type for the group — see Pitfalls.

4. **Config doesn't show up in running-config.** `aruba_get_config` with a `section` keyword first → if that section comes back empty, fall back to `aruba_get_node_config` with the matching `object_type` — SSID/VAP/role config pushed from the conductor to managed devices often isn't visible via `running-config` at all.

5. **Ad hoc diagnostic command.** `aruba_show` with any `show ...` command for one-off checks (`show version`, `show ap database long`, etc). Treat the result as conductor-scoped even if you pass `node` — see Pitfalls.

## Pitfalls & limits

- **Never call `aruba_get_node_config` with only a group-level `config_path` and no narrow `object_type`, and never loop it across every object type for a group.** An unscoped group-level pull (e.g. `/md/<group-name>`) can return 90KB+ of JSON in a single response and will blow the context window. Always pass one specific `object_type` (`ssid_prof`, `virtual_ap`, `ap_group`, `role`, etc.) per call, and prefer the smallest group scope that answers the question.
- `aruba_get_node_config` only accepts group-level `config_path` values (e.g. `/md`, `/md/<group-name>`). A leaf-level path pointing at an individual managed device returns `{"Error": "Invalid config path"}` — don't target a specific controller hostname in `config_path`.
- The `node` parameter on `aruba_show` is unreliable — targeting a specific managed device can silently return the conductor's own result instead of erroring. Don't trust `aruba_show` output as device-specific just because `node` was set; confirm against an obviously device-specific field (model, uptime) before relying on it.
- `aruba_show` commands must start with `show ` (validated server-side) — other AOS CLI syntax is rejected.
- This server is read-only against a production wireless controller — there is no create/update/delete tool of any kind.
- Failed or empty API calls come back as `{"_error": "..."}` or `{"_raw": "..."}` inside the JSON rather than raising — check for those keys before treating a result as structured data.
- SSID/VAP/role configuration pushed from the conductor to managed devices frequently doesn't appear via `aruba_get_config`'s running-config section search — use `aruba_get_node_config` for those object types instead.
- The server authenticates with a single shared read-only service account — there's no per-user credential, so don't ask IT staff for their own controller login.

## Setup

See `../../SECRETS-SETUP.md` for credential setup. This server needs `ARUBA_HOST`, `ARUBA_USERNAME`, `ARUBA_PASSWORD` (optional: `ARUBA_PORT`, defaults to `4343`; `ARUBA_VERIFY_SSL`, defaults to `false`).
Loading
Loading