From 150b262db3dfb9247ef2868529d3de3961cf1eed Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 29 Aug 2026 20:46:00 -0700 Subject: [PATCH 1/2] =?UTF-8?q?Remove=20orphaned=20mcpo/config.json=20mirr?= =?UTF-8?q?or=20=E2=80=94=20migrated=20to=20homelab/mcp-config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This file was never actually deployed by this repo's pipeline (ai.yaml bind-mounts /volume1/docker/mcpo/config.json on the host directly; nothing here ever synced it there — see homelab/mcp-config README for full writeup). It also contained several real plaintext credentials (Proxmox token, Authentik JWT/token, UniFi password, Gitea tokens) that have since been rotated and templated in the new repo. NOTE: deleting this from HEAD does not remove it from compose-files' git history. The rotated credentials must be treated as permanently compromised regardless of this deletion. --- mcpo/config.json | 186 ----------------------------------------------- 1 file changed, 186 deletions(-) delete mode 100644 mcpo/config.json diff --git a/mcpo/config.json b/mcpo/config.json deleted file mode 100644 index 6b51516..0000000 --- a/mcpo/config.json +++ /dev/null @@ -1,186 +0,0 @@ -{ - "mcpServers": { - "filesystem": { - "command": "npx", - "args": [ - "-y", - "@modelcontextprotocol/server-filesystem", - "/mcpo_data/filesystem" - ] - }, - "memory": { - "command": "npx", - "args": [ - "-y", - "@modelcontextprotocol/server-memory" - ] - }, - "proxmox-nuck7-1": { - "command": "sh", - "args": [ - "-c", - "LOG_LEVEL=silent npx -y --package @bldg-7/proxmox-mcp@1.2.1 --package pino-pretty proxmox-mcp 2>/dev/null" - ], - "env": { - "PROXMOX_HOST": "192.168.4.11", - "PROXMOX_PORT": "8006", - "PROXMOX_USER": "MCP@pve", - "PROXMOX_TOKEN_NAME": "MCP", - "PROXMOX_TOKEN_VALUE": "2052990e-749f-43f6-be7f-c9ad206281cc", - "PROXMOX_SSL_MODE": "insecure", - "PROXMOX_ALLOW_ELEVATED": "true", - "PROXMOX_SSH_ENABLED": "true", - "PROXMOX_SSH_HOST": "192.168.4.11", - "PROXMOX_SSH_PORT": "22", - "PROXMOX_SSH_USER": "root", - "PROXMOX_SSH_KEY_PATH": "/app/ssh_keys/nuc-cluster", - "PROXMOX_SSH_NODE": "nuck7-1", - "PROXMOX_ALLOW_UNSAFE_COMMANDS": "true" - } - }, - "proxmox-nuck7-2": { - "command": "sh", - "args": [ - "-c", - "LOG_LEVEL=silent npx -y --package @bldg-7/proxmox-mcp@1.2.1 --package pino-pretty proxmox-mcp 2>/dev/null" - ], - "env": { - "PROXMOX_HOST": "192.168.4.12", - "PROXMOX_PORT": "8006", - "PROXMOX_USER": "MCP@pve", - "PROXMOX_TOKEN_NAME": "MCP", - "PROXMOX_TOKEN_VALUE": "2052990e-749f-43f6-be7f-c9ad206281cc", - "PROXMOX_SSL_MODE": "insecure", - "PROXMOX_ALLOW_ELEVATED": "true", - "PROXMOX_SSH_ENABLED": "true", - "PROXMOX_SSH_HOST": "192.168.4.12", - "PROXMOX_SSH_PORT": "22", - "PROXMOX_SSH_USER": "root", - "PROXMOX_SSH_KEY_PATH": "/app/ssh_keys/nuc-cluster", - "PROXMOX_SSH_NODE": "nuck7-2", - "PROXMOX_ALLOW_UNSAFE_COMMANDS": "true" - } - }, - "proxmox-nuck7-3": { - "command": "sh", - "args": [ - "-c", - "LOG_LEVEL=silent npx -y --package @bldg-7/proxmox-mcp@1.2.1 --package pino-pretty proxmox-mcp 2>/dev/null" - ], - "env": { - "PROXMOX_HOST": "192.168.4.13", - "PROXMOX_PORT": "8006", - "PROXMOX_USER": "MCP@pve", - "PROXMOX_TOKEN_NAME": "MCP", - "PROXMOX_TOKEN_VALUE": "2052990e-749f-43f6-be7f-c9ad206281cc", - "PROXMOX_SSL_MODE": "insecure", - "PROXMOX_ALLOW_ELEVATED": "true", - "PROXMOX_SSH_ENABLED": "true", - "PROXMOX_SSH_HOST": "192.168.4.13", - "PROXMOX_SSH_PORT": "22", - "PROXMOX_SSH_USER": "root", - "PROXMOX_SSH_KEY_PATH": "/app/ssh_keys/nuc-cluster", - "PROXMOX_SSH_NODE": "nuck7-3", - "PROXMOX_ALLOW_UNSAFE_COMMANDS": "true" - } - }, - "homeassistant": { - "command": "npx", - "args": [ - "-y", - "mcp-remote", - "https://home.bryanmail.net/mcp_server/sse", - "--header", - "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIxNzhmYzI0NjA2Y2I0ZTg4ODI1N2VmMzdiZTNhY2E5YSIsImlhdCI6MTc3NDY4MzAxNiwiZXhwIjoyMDkwMDQzMDE2fQ.32kY2LVHzKZHWLc96T6z2P-8beNTnp2DHRUf2UEie2w" - ] - }, - "teams": { - "command": "npx", - "args": [ - "-y", - "@floriscornel/teams-mcp@latest" - ], - "env": { - "TEAMS_MCP_READ_ONLY": "true", - "HOME": "/app/teams-mcp-auth" - } - }, - "ms365": { - "command": "npx", - "args": [ - "-y", - "@softeria/ms-365-mcp-server@0.129.0", - "--preset", - "personal", - "--read-only", - "--discovery" - ], - "env": { - "HOME": "/app/teams-mcp-auth", - "MS365_MCP_CLIENT_ID": "14d82eec-204b-4c2f-b7e8-296a70dab67e", - "MS365_MCP_TOKEN_CACHE_PATH": "/app/teams-mcp-auth/.teams-mcp-token-cache.json", - "SILENT": "true" - } - }, - "unifi-network": { - "command": "uvx", - "args": [ - "unifi-network-mcp@latest" - ], - "env": { - "UNIFI_HOST": "192.168.4.1", - "UNIFI_USERNAME": "unifi-mcp", - "UNIFI_PASSWORD": "3dHOEOMygTYeX3", - "UNIFI_PORT": "443", - "UNIFI_VERIFY_SSL": "false", - "UV_CACHE_DIR": "/app/uv-cache" - } - }, - "authentik": { - "command": "uvx", - "args": [ - "authentik-diag-mcp", - "--base-url", - "https://auth.bryanmail.net", - "--token", - "LAm3lBTumOmsU8AiFQM2FmCZyoj8bTSR0FQnAcBy1QnTMEyU4oWozwdxTUap" - ] - }, - "gitea": { - "command": "/mcpo_data/gitea-mcp", - "args": [ - "-t", - "stdio" - ], - "env": { - "GITEA_HOST": "https://git.bryanmail.net", - "GITEA_ACCESS_TOKEN": "5aa3a554c001b1dbe5215cb8cb388112801930e9" - } - }, - "gitea-admin": { - "command": "/mcpo_data/gitea-mcp", - "args": [ - "-t", - "stdio" - ], - "env": { - "GITEA_HOST": "https://git.bryanmail.net", - "GITEA_ACCESS_TOKEN": "289225b0b8f1827242191874b2408db76af06321" - } - }, - "powerautomate": { - "command": "npx", - "args": [ - "-y", - "powerautomate-mcp@latest", - "--stdio" - ], - "env": { - "HOME": "/app/powerautomate-auth", - "PA_MCP_CLIENT_ID": "84b431ed-ef5d-48a0-b0a1-878cfdb71453", - "PA_MCP_TENANT_ID": "0f6cf991-c449-480a-a71b-83003ce6edc1", - "PA_CONFIG_PATH": "/app/powerautomate-auth/config.json" - } - } - } -} From b6b23a79702aeecf09bfc676c6722ba26b942bc8 Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 29 Aug 2026 20:46:01 -0700 Subject: [PATCH 2/2] =?UTF-8?q?Remove=20mcpo/woodpecker-mcp.mjs=20?= =?UTF-8?q?=E2=80=94=20migrated=20unchanged=20to=20homelab/mcp-config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mcpo/woodpecker-mcp.mjs | 217 ---------------------------------------- 1 file changed, 217 deletions(-) delete mode 100644 mcpo/woodpecker-mcp.mjs diff --git a/mcpo/woodpecker-mcp.mjs b/mcpo/woodpecker-mcp.mjs deleted file mode 100644 index a151666..0000000 --- a/mcpo/woodpecker-mcp.mjs +++ /dev/null @@ -1,217 +0,0 @@ -#!/usr/bin/env node -// Minimal MCP server (stdio, JSON-RPC 2.0) for Woodpecker CI. -// Zero dependencies — plain Node 18+ (built-in fetch). Config via env vars. -// -// Configuration (env): -// WOODPECKER_URL e.g. https://your-woodpecker.example.com -// WOODPECKER_TOKEN Woodpecker Personal Access Token (JWT) -// -// Tools: -// woodpecker_list_repos -// woodpecker_list_pipelines { repo_id, limit? } -// woodpecker_get_pipeline { repo_id, number } -> status + workflow/step tree -// woodpecker_pipeline_logs { repo_id, number, step_id, tail? } -> decoded step logs - -import { Buffer } from "node:buffer"; - -const BASE = (process.env.WOODPECKER_URL || "").replace(/\/+$/, ""); -const TOKEN = process.env.WOODPECKER_TOKEN || ""; - -function log(...a) { - // Diagnostics go to stderr so they never corrupt the stdout JSON-RPC stream. - process.stderr.write("[woodpecker-mcp] " + a.join(" ") + "\n"); -} - -async function api(path) { - if (!BASE || !TOKEN) throw new Error("Missing WOODPECKER_URL or WOODPECKER_TOKEN in environment"); - const res = await fetch(`${BASE}/api${path}`, { - headers: { Authorization: `Bearer ${TOKEN}`, Accept: "application/json" }, - }); - const body = await res.text(); - if (!res.ok) throw new Error(`HTTP ${res.status} ${path}: ${body.slice(0, 300)}`); - return body ? JSON.parse(body) : null; -} - -// --- tool response formatting --- - -function summarizePipeline(p) { - const lines = []; - lines.push(`pipeline #${p.number} (id=${p.id}) status=${p.status} event=${p.event} branch=${p.branch}`); - lines.push(`commit=${(p.commit || "").slice(0, 12)} author=${p.author}`); - if (p.message) lines.push(`message: ${p.message.split("\n")[0]}`); - for (const wf of p.workflows || []) { - lines.push(` WORKFLOW "${wf.name}" (pid=${wf.pid}) state=${wf.state}${wf.error ? ` error=${wf.error}` : ""}`); - for (const c of wf.children || []) { - lines.push( - ` step "${c.name}" (id=${c.id}, pid=${c.pid}) ${c.state} exit=${c.exit_code ?? "-"} type=${c.type}` - ); - } - } - return lines.join("\n"); -} - -function decodeLogs(entries) { - if (!Array.isArray(entries)) return String(entries); - return entries - .map((e) => { - const d = e?.data; - if (d == null) return ""; - try { - return Buffer.from(d, "base64").toString("utf-8"); - } catch { - return String(d); - } - }) - .join(""); -} - -const TOOLS = [ - { - name: "woodpecker_list_repos", - description: "List repositories the token can access (id, full name, default branch).", - inputSchema: { type: "object", properties: {}, additionalProperties: false }, - handler: async () => { - const repos = await api(`/user/repos`); - return (repos || []) - .map((r) => `id=${r.id} ${r.full_name} default_branch=${r.default_branch}`) - .join("\n") || "(no repositories)"; - }, - }, - { - name: "woodpecker_list_pipelines", - description: "Recent pipelines for a repository. Params: repo_id (number), limit (number, default 20).", - inputSchema: { - type: "object", - properties: { - repo_id: { type: "number", description: "Woodpecker repository ID" }, - limit: { type: "number", description: "How many pipelines to return (default 20)" }, - }, - required: ["repo_id"], - additionalProperties: false, - }, - handler: async ({ repo_id, limit }) => { - const list = await api(`/repos/${repo_id}/pipelines?perPage=${limit || 20}`); - return (list || []) - .map( - (p) => - `#${p.number} ${p.status.padEnd(8)} ${p.event.padEnd(12)} ${p.branch} ${(p.commit || "").slice(0, 8)} ${(p.message || "").split("\n")[0]}` - ) - .join("\n") || "(no pipelines)"; - }, - }, - { - name: "woodpecker_get_pipeline", - description: "Pipeline details: status plus the workflow/step tree (with step ids for fetching logs). Params: repo_id, number.", - inputSchema: { - type: "object", - properties: { - repo_id: { type: "number" }, - number: { type: "number", description: "Pipeline number (as shown in the UI)" }, - }, - required: ["repo_id", "number"], - additionalProperties: false, - }, - handler: async ({ repo_id, number }) => { - const p = await api(`/repos/${repo_id}/pipelines/${number}`); - return summarizePipeline(p); - }, - }, - { - name: "woodpecker_pipeline_logs", - description: "Decoded logs for a single step. Params: repo_id, number (pipeline), step_id (from woodpecker_get_pipeline). Optional tail (last N lines).", - inputSchema: { - type: "object", - properties: { - repo_id: { type: "number" }, - number: { type: "number" }, - step_id: { type: "number", description: "Step id from woodpecker_get_pipeline" }, - tail: { type: "number", description: "Return only the last N lines (optional)" }, - }, - required: ["repo_id", "number", "step_id"], - additionalProperties: false, - }, - handler: async ({ repo_id, number, step_id, tail }) => { - const entries = await api(`/repos/${repo_id}/logs/${number}/${step_id}`); - let txt = decodeLogs(entries); - if (tail && tail > 0) { - txt = txt.split("\n").slice(-tail).join("\n"); - } - return txt || "(no logs)"; - }, - }, -]; - -// --- JSON-RPC over stdio loop --- - -function send(msg) { - process.stdout.write(JSON.stringify(msg) + "\n"); -} - -function reply(id, result) { - send({ jsonrpc: "2.0", id, result }); -} - -function replyError(id, code, message) { - send({ jsonrpc: "2.0", id, error: { code, message } }); -} - -async function handle(req) { - const { id, method, params } = req; - if (method === "initialize") { - reply(id, { - protocolVersion: params?.protocolVersion || "2024-11-05", - capabilities: { tools: {} }, - serverInfo: { name: "woodpecker-mcp", version: "1.0.0" }, - }); - return; - } - if (method === "notifications/initialized" || method === "notifications/cancelled") { - return; // notifications carry no response - } - if (method === "ping") { - reply(id, {}); - return; - } - if (method === "tools/list") { - reply(id, { - tools: TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })), - }); - return; - } - if (method === "tools/call") { - const tool = TOOLS.find((t) => t.name === params?.name); - if (!tool) { - replyError(id, -32602, `Unknown tool: ${params?.name}`); - return; - } - try { - const text = await tool.handler(params.arguments || {}); - reply(id, { content: [{ type: "text", text }] }); - } catch (e) { - reply(id, { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true }); - } - return; - } - if (id !== undefined) replyError(id, -32601, `Unsupported method: ${method}`); -} - -let buf = ""; -process.stdin.setEncoding("utf-8"); -process.stdin.on("data", (chunk) => { - buf += chunk; - let nl; - while ((nl = buf.indexOf("\n")) >= 0) { - const line = buf.slice(0, nl).trim(); - buf = buf.slice(nl + 1); - if (!line) continue; - let req; - try { - req = JSON.parse(line); - } catch { - continue; - } - handle(req).catch((e) => log("handler error:", e.message)); - } -}); -process.stdin.on("end", () => process.exit(0)); -log("ready", BASE ? `(${BASE})` : "(WOODPECKER_URL is not set!)");