Add plugin builder for one-file client install

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 08:39:27 -06:00
parent 4f9a862c25
commit 549a1b6879
7 changed files with 195 additions and 0 deletions

4
.gitignore vendored
View File

@@ -6,3 +6,7 @@ __pycache__/
# MCP server runtime secrets
mcp-server/.env
# Personalized plugin builds contain API keys — never commit
*.plugin
!freshservice-skill.zip

46
plugin/README.md Normal file
View File

@@ -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.

49
plugin/make-plugin.ps1 Normal file
View File

@@ -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."

55
plugin/make-plugin.sh Executable file
View File

@@ -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."

View File

@@ -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"
}
}

13
plugin/template/.mcp.json Normal file
View File

@@ -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__"
}
}
}
}

20
plugin/template/README.md Normal file
View File

@@ -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).