Author SHA1 Message Date
admin b6b23a7970 Remove mcpo/woodpecker-mcp.mjs — migrated unchanged to homelab/mcp-config 2026-08-29 20:46:01 -07:00
admin 150b262db3 Remove orphaned mcpo/config.json mirror — migrated to homelab/mcp-config
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.
2026-08-29 20:46:00 -07:00
5 changed files with 0 additions and 507 deletions
-186
View File
@@ -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"
}
}
}
}
-21
View File
@@ -1,21 +0,0 @@
FROM ghcr.io/open-webui/mcpo:main
# Azure CLI — required for non-interactive service-principal auth
# (`az login --service-principal`) performed by entrypoint.sh at container
# start. FlowAgent's auth model is Azure CLI + MSAL; see
# https://github.com/microsoft/power-platform-skills/blob/main/plugins/power-automate/references/connection-patterns.md
RUN apk add --no-cache py3-pip curl \
&& pip install --no-cache-dir --break-system-packages azure-cli
# FlowAgent self-contained MCP bundle (stdio transport, all tools inlined,
# Node 18+ only — no npm install / remote host needed at runtime).
# Pinned to a ref (commit SHA or tag), NOT `main`, for reproducible builds.
# Bump deliberately via PR when upstream ships updates:
# https://github.com/microsoft/power-platform-skills/tree/main/plugins/power-automate/server
ARG FLOWAGENT_REF=main
RUN mkdir -p /app/flowagent \
&& curl -fsSL "https://raw.githubusercontent.com/microsoft/power-platform-skills/${FLOWAGENT_REF}/plugins/power-automate/server/mcp.mjs" \
-o /app/flowagent/mcp.mjs
COPY entrypoint.sh /app/flowagent/entrypoint.sh
RUN chmod +x /app/flowagent/entrypoint.sh
-30
View File
@@ -1,30 +0,0 @@
#!/bin/sh
# FlowAgent MCP entrypoint — non-interactive Azure service-principal login.
#
# Expects three env vars pointing at Docker secret files (Pattern C,
# _FILE convention, provisioned by Woodpecker — never hand-typed):
# FLOWAGENT_AZURE_CLIENT_ID_FILE
# FLOWAGENT_AZURE_TENANT_ID_FILE
# FLOWAGENT_AZURE_CLIENT_SECRET_FILE
#
# On success, execs into the real command (node /app/flowagent/mcp.mjs),
# replacing this shell so mcpo's stdio pipe talks directly to the MCP process.
set -eu
: "${FLOWAGENT_AZURE_CLIENT_ID_FILE:?FLOWAGENT_AZURE_CLIENT_ID_FILE not set}"
: "${FLOWAGENT_AZURE_TENANT_ID_FILE:?FLOWAGENT_AZURE_TENANT_ID_FILE not set}"
: "${FLOWAGENT_AZURE_CLIENT_SECRET_FILE:?FLOWAGENT_AZURE_CLIENT_SECRET_FILE not set}"
CLIENT_ID="$(cat "$FLOWAGENT_AZURE_CLIENT_ID_FILE")"
TENANT_ID="$(cat "$FLOWAGENT_AZURE_TENANT_ID_FILE")"
CLIENT_SECRET="$(cat "$FLOWAGENT_AZURE_CLIENT_SECRET_FILE")"
az login --service-principal \
-u "$CLIENT_ID" \
-p "$CLIENT_SECRET" \
--tenant "$TENANT_ID" \
--output none
unset CLIENT_SECRET
exec "$@"
-217
View File
@@ -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!)");
-53
View File
@@ -1,53 +0,0 @@
# flowagent (Power Automate MCP, via mcpo service) — Secrets Reference
# Source: mcpo/flowagent — built into a custom image, consumed by the
# "mcpo" service in ai.yaml (NEVER mcpo-critical).
#
# Add SECRET values to Woodpecker at:
# https://woodpecker.bryanmail.net
# homelab/compose-files → Settings → Secrets
#
# Prerequisite (manual, outside GitOps — Azure Portal):
# 1. Entra ID → App registrations → New registration
# Name: flowagent-mcp-homelab, single tenant
# 2. API permissions → add Power Automate / Flow Service application
# permissions + Dynamics CRM user_impersonation (Dataverse access)
# 3. Grant admin consent
# 4. Certificates & secrets → new client secret → copy value immediately
# 5. Power Platform Admin Center → target environment → S2S apps →
# register the application user for this app ID (required for
# Dataverse/environment access by a service principal)
#
# NOTE: the OLD "powerautomate" mcpo/config.json entry (npm package
# powerautomate-mcp, client_id 84b431ed-..., tenant_id 0f6cf991-...) was
# never onboarded to this secrets pipeline and is being replaced by this.
# Do not reuse those IDs unless you've independently confirmed in Azure
# Portal that the old App Registration still exists, still has valid
# permissions/consent, and you intend to reuse it — otherwise register new.
# ── SECRETS (add to Woodpecker) ──────────────────────────────────────────
# Woodpecker secret name: flowagent_azure_client_id
# Used for: Azure AD App Registration client ID
# Env var in entrypoint: FLOWAGENT_AZURE_CLIENT_ID_FILE (Docker secret _FILE)
flowagent_azure_client_id=
# Woodpecker secret name: flowagent_azure_tenant_id
# Used for: Azure AD tenant ID
# Env var in entrypoint: FLOWAGENT_AZURE_TENANT_ID_FILE (Docker secret _FILE)
flowagent_azure_tenant_id=
# Woodpecker secret name: flowagent_azure_client_secret
# Used for: Azure AD App Registration client secret (rotate if leaked)
# Env var in entrypoint: FLOWAGENT_AZURE_CLIENT_SECRET_FILE (Docker secret _FILE)
flowagent_azure_client_secret=
# ── Woodpecker provision-secrets case entry ──────────────────────────────
# Add this to the provision-secrets step in .woodpecker/deploy.yml
# (separate follow-up PR — this file only documents it):
#
# ai)
# ... existing ai-stack secret provisioning ...
# create_or_update_secret "flowagent_azure_client_id" "$FLOWAGENT_AZURE_CLIENT_ID"
# create_or_update_secret "flowagent_azure_tenant_id" "$FLOWAGENT_AZURE_TENANT_ID"
# create_or_update_secret "flowagent_azure_client_secret" "$FLOWAGENT_AZURE_CLIENT_SECRET"
# ;;