From 549a1b6879a764137ae9970c76f7e0ebd3029f49 Mon Sep 17 00:00:00 2001 From: spencerm Date: Fri, 10 Jul 2026 08:39:27 -0600 Subject: [PATCH] Add plugin builder for one-file client install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit plugin/make-plugin.{sh,ps1} stamp a user's own Freshservice API key and the shared server token into a template and emit a personalized freshservice.plugin (direct HTTP MCP connection — no Node/mcp-remote/config editing on clients). Built plugins embed credentials, so *.plugin is git-ignored; the committed template holds only placeholders. Validated with 'claude plugin validate'. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 4 ++ plugin/README.md | 46 ++++++++++++++++++ plugin/make-plugin.ps1 | 49 +++++++++++++++++++ plugin/make-plugin.sh | 55 ++++++++++++++++++++++ plugin/template/.claude-plugin/plugin.json | 8 ++++ plugin/template/.mcp.json | 13 +++++ plugin/template/README.md | 20 ++++++++ 7 files changed, 195 insertions(+) create mode 100644 plugin/README.md create mode 100644 plugin/make-plugin.ps1 create mode 100755 plugin/make-plugin.sh create mode 100644 plugin/template/.claude-plugin/plugin.json create mode 100644 plugin/template/.mcp.json create mode 100644 plugin/template/README.md diff --git a/.gitignore b/.gitignore index 63e5971..301c648 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,7 @@ __pycache__/ # MCP server runtime secrets mcp-server/.env + +# Personalized plugin builds contain API keys — never commit +*.plugin +!freshservice-skill.zip diff --git a/plugin/README.md b/plugin/README.md new file mode 100644 index 0000000..55e252e --- /dev/null +++ b/plugin/README.md @@ -0,0 +1,46 @@ +# Freshservice plugin builder + +Generates a personalized **`freshservice.plugin`** that connects Claude +Desktop / Cowork to the shared Freshservice MCP server on the LAN — no Node.js, +no `mcp-remote`, no hand-editing `claude_desktop_config.json`. The plugin uses +a direct HTTP MCP connection with your own identity headers, so everything you +do in Freshservice is attributed to **you**. + +## For coworkers: get connected in three steps + +1. **Get your Freshservice API key**: Freshservice → profile picture → + *Profile settings* → API key. Also ask IT (Spencer) for the **shared server + token**. + +2. **Build your plugin** (from a clone of this repo): + + macOS / Linux: + ```bash + cd plugin && ./make-plugin.sh + ``` + + Windows (PowerShell): + ```powershell + cd plugin + powershell -ExecutionPolicy Bypass -File .\make-plugin.ps1 + ``` + + Enter your API key and the shared token when prompted. This produces + `freshservice.plugin` **containing your key — don't share the file.** + +3. **Install it**: in Claude Desktop, go to Cowork → **Customize** → add the + `.plugin` file. Start a new chat; the Freshservice tools are available. + +## Requirements + +- Agent account in the PESCO Freshservice instance +- Network reach to `192.168.101.12:3839` (on-site or VPN) + +## Notes + +- Defaults (server URL, domain) can be overridden via env vars: + `SERVER_URL=... FS_DOMAIN=... ./make-plugin.sh` +- Built `.plugin` files are git-ignored on purpose — they embed credentials. +- The server side of this lives in [`../mcp-server/`](../mcp-server/); it must + be running in multi-user mode (no `FRESHSERVICE_*` env baked in) for + per-user identity to be enforced. diff --git a/plugin/make-plugin.ps1 b/plugin/make-plugin.ps1 new file mode 100644 index 0000000..6f40f03 --- /dev/null +++ b/plugin/make-plugin.ps1 @@ -0,0 +1,49 @@ +# Build a personalized freshservice.plugin for Claude Desktop / Cowork (Windows). +# +# Each person runs this ONCE with their own Freshservice API key. The output +# .plugin file contains your key — do not share it. +# +# Usage: powershell -ExecutionPolicy Bypass -File .\make-plugin.ps1 +$ErrorActionPreference = "Stop" + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$Template = Join-Path $ScriptDir "template" + +$ServerUrl = if ($env:SERVER_URL) { $env:SERVER_URL } else { "http://192.168.101.12:3839/mcp" } +$FsDomain = if ($env:FS_DOMAIN) { $env:FS_DOMAIN } else { "pesco.freshservice.com" } + +$FsKey = if ($env:FS_KEY) { $env:FS_KEY } else { + Read-Host "Your Freshservice API key (Freshservice -> profile picture -> Profile settings -> API key)" +} +$McpToken = if ($env:MCP_TOKEN) { $env:MCP_TOKEN } else { + Read-Host "Shared MCP server token (ask IT / Spencer)" +} +if (-not $FsKey) { throw "API key is required." } +if (-not $McpToken) { throw "MCP token is required." } + +$Build = Join-Path ([System.IO.Path]::GetTempPath()) ("fsplugin-" + [guid]::NewGuid()) +Copy-Item -Recurse -Force $Template $Build + +$McpJson = Join-Path $Build ".mcp.json" +$content = Get-Content -Raw $McpJson +$content = $content.Replace("__SERVER_URL__", $ServerUrl) +$content = $content.Replace("__FS_DOMAIN__", $FsDomain) +$content = $content.Replace("__FS_KEY__", $FsKey) +$content = $content.Replace("__MCP_TOKEN__", $McpToken) +Set-Content -NoNewline -Path $McpJson -Value $content + +# Sanity check: no placeholders left anywhere. +$leftover = Get-ChildItem -Recurse -File $Build | Select-String -Pattern "__[A-Z_]*__" -SimpleMatch:$false +if ($leftover) { throw "Unfilled placeholders remain; aborting." } + +$Out = Join-Path $ScriptDir "freshservice.plugin" +if (Test-Path $Out) { Remove-Item $Out } +Add-Type -AssemblyName System.IO.Compression.FileSystem +# CreateFromDirectory includes dot-directories (.claude-plugin), unlike +# Compress-Archive with a wildcard path. +[System.IO.Compression.ZipFile]::CreateFromDirectory($Build, $Out) +Remove-Item -Recurse -Force $Build + +Write-Host "Built: $Out" +Write-Host "Install it in Claude Desktop (Cowork -> Customize -> add plugin), then start a new chat." +Write-Host "REMINDER: this file contains your personal API key -- don't share it." diff --git a/plugin/make-plugin.sh b/plugin/make-plugin.sh new file mode 100755 index 0000000..ca762cf --- /dev/null +++ b/plugin/make-plugin.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Build a personalized freshservice.plugin for Claude Desktop / Cowork. +# +# Each person runs this ONCE with their own Freshservice API key. The output +# .plugin file contains your key — do not share it. +# +# Usage (interactive): +# ./make-plugin.sh +# Or non-interactive: +# FS_KEY=... MCP_TOKEN=... ./make-plugin.sh +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TEMPLATE="$SCRIPT_DIR/template" + +SERVER_URL="${SERVER_URL:-http://192.168.101.12:3839/mcp}" +FS_DOMAIN="${FS_DOMAIN:-pesco.freshservice.com}" + +if [ -z "${FS_KEY:-}" ]; then + printf "Your Freshservice API key (Freshservice → profile picture → Profile settings → API key): " + read -r FS_KEY +fi +if [ -z "${MCP_TOKEN:-}" ]; then + printf "Shared MCP server token (ask IT / Spencer): " + read -r MCP_TOKEN +fi +[ -n "$FS_KEY" ] || { echo "ERROR: API key is required." >&2; exit 1; } +[ -n "$MCP_TOKEN" ] || { echo "ERROR: MCP token is required." >&2; exit 1; } + +BUILD="$(mktemp -d)" +trap 'rm -rf "$BUILD"' EXIT +cp -R "$TEMPLATE/." "$BUILD/" + +# Stamp placeholders using bash substitution (safe for special characters). +for f in "$BUILD/.mcp.json"; do + content="$(cat "$f")" + content="${content//__SERVER_URL__/$SERVER_URL}" + content="${content//__FS_DOMAIN__/$FS_DOMAIN}" + content="${content//__FS_KEY__/$FS_KEY}" + content="${content//__MCP_TOKEN__/$MCP_TOKEN}" + printf '%s' "$content" > "$f" +done + +# Sanity check: no placeholders left anywhere. +if grep -rq "__[A-Z_]*__" "$BUILD"; then + echo "ERROR: unfilled placeholders remain; aborting." >&2 + exit 1 +fi + +OUT="$SCRIPT_DIR/freshservice.plugin" +rm -f "$OUT" +(cd "$BUILD" && zip -r -q "$OUT" . -x "*.DS_Store") +echo "Built: $OUT" +echo "Install it in Claude Desktop (Cowork → Customize → add plugin), then start a new chat." +echo "REMINDER: this file contains your personal API key — don't share it." diff --git a/plugin/template/.claude-plugin/plugin.json b/plugin/template/.claude-plugin/plugin.json new file mode 100644 index 0000000..3717458 --- /dev/null +++ b/plugin/template/.claude-plugin/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "freshservice", + "version": "1.0.0", + "description": "Connect Claude to the PESCO Freshservice service desk — tickets, assets/CMDB, people, and changes/problems/releases — through the shared LAN MCP server, acting as your own agent identity.", + "author": { + "name": "Spencer McGuire" + } +} diff --git a/plugin/template/.mcp.json b/plugin/template/.mcp.json new file mode 100644 index 0000000..1cd3947 --- /dev/null +++ b/plugin/template/.mcp.json @@ -0,0 +1,13 @@ +{ + "mcpServers": { + "freshservice": { + "type": "http", + "url": "__SERVER_URL__", + "headers": { + "Authorization": "Bearer __MCP_TOKEN__", + "X-Freshservice-Domain": "__FS_DOMAIN__", + "X-Freshservice-Key": "__FS_KEY__" + } + } + } +} diff --git a/plugin/template/README.md b/plugin/template/README.md new file mode 100644 index 0000000..88c69eb --- /dev/null +++ b/plugin/template/README.md @@ -0,0 +1,20 @@ +# Freshservice plugin + +Connects Claude (Desktop / Cowork) to the PESCO Freshservice service desk +through the shared MCP server on the LAN. Tickets, assets/CMDB, people, and +change/problem/release records — with every action attributed to **your own** +Freshservice agent account. + +## This file is personalized + +This plugin was generated by `make-plugin` and contains **your** Freshservice +API key and the shared server token. Treat it like a password: + +- Do **not** share your `.plugin` file with anyone else — they would act as you. +- If your key is exposed, regenerate it in Freshservice (Profile settings → + API key) and rebuild the plugin. + +## Requirements + +- You must be an agent in the PESCO Freshservice instance. +- Your computer must be able to reach the LAN server (on-site network or VPN).