Add MCP servers (Gitea, K8s, Postgres, filesystem, git) to harness agents
Some checks failed
CI / lint-and-test (push) Successful in 36s
Deploy Production / deploy (push) Failing after 40s
CI / build (push) Failing after 59s

Wire 5 MCP servers into Claude Code agents spawned by the harness:
- Gitea MCP for repo/issue/PR management on self-hosted Gitea
- Kubernetes MCP with read-only RBAC for cluster inspection
- Postgres MCP with read-only user for database queries
- Filesystem and Git MCP scoped to task worktrees

Generates .claude/settings.json in each worktree before agent spawn.
Gracefully skips for Codex/OpenCode runtimes (no MCP support).

Also fixes node-pty build failure by using local Node.js headers
instead of downloading from unofficial-builds.nodejs.org (ECONNRESET).
This commit is contained in:
Julia McGhee
2026-03-21 20:55:19 +00:00
parent a5ef56b052
commit 620fbc6b83
9 changed files with 192 additions and 1 deletions

View File

@@ -82,6 +82,7 @@ async function loadCredentialsFromEnv() {
["GH_TOKEN", "github", "GitHub (env)"],
["GITLAB_TOKEN", "gitlab", "GitLab (env)"],
["GITEA_TOKEN", "gitea", "Gitea (env)"],
["HARNESS_PG_MCP_URL", "postgres-mcp", "Postgres MCP (env)"],
];
for (const [envVar, provider, label] of envMap) {
@@ -91,6 +92,17 @@ async function loadCredentialsFromEnv() {
if (existing.length > 0) continue;
await upsertCredential({ id: `env-${provider}`, provider, label, token });
}
// Set Gitea baseUrl from GITEA_URL if the credential exists
const giteaUrl = process.env.GITEA_URL;
if (giteaUrl) {
const giteaCreds = await getRawCredentialsByProvider("gitea");
for (const cred of giteaCreds) {
if (!cred.baseUrl) {
await upsertCredential({ ...cred, baseUrl: giteaUrl });
}
}
}
}
async function loadClaudeCredentials() {

View File

@@ -4,10 +4,12 @@ import { credentials as credentialsTable } from "@homelab/db";
export type Provider =
| "github" | "gitlab" | "gitea"
| "anthropic" | "openai" | "openrouter" | "google" | "opencode-zen" | "opencode-go";
| "anthropic" | "openai" | "openrouter" | "google" | "opencode-zen" | "opencode-go"
| "postgres-mcp";
export const GIT_PROVIDERS: Provider[] = ["github", "gitlab", "gitea"];
export const AI_PROVIDERS: Provider[] = ["anthropic", "openai", "openrouter", "google", "opencode-zen", "opencode-go"];
export const INFRA_PROVIDERS: Provider[] = ["postgres-mcp"];
export interface Credential {
id: string;

View File

@@ -1,6 +1,7 @@
import { spawn, ChildProcess } from "node:child_process";
import { getAgentConfig, buildAgentCommand, AGENT_RUNTIMES } from "./agents";
import { buildAgentEnv } from "./agent-env";
import { generateMcpConfig } from "./mcp-config";
import { ExecutionResult } from "./types";
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
@@ -44,6 +45,9 @@ export async function executeAgent(opts: {
const env = await buildAgentEnv(config);
// Write .claude/settings.json with MCP servers for claude-code agents
await generateMcpConfig(opts.workDir, config.runtime);
const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const startTime = Date.now();

View File

@@ -0,0 +1,93 @@
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { getRawCredentialsByProvider } from "./credentials";
import type { AgentRuntime } from "./agents";
const DEFAULT_GITEA_URL = "http://gitea.platform.svc:3000";
interface McpServerDef {
command: string;
args: string[];
env?: Record<string, string>;
}
interface ClaudeProjectSettings {
permissions: {
allow: string[];
deny: string[];
};
mcpServers: Record<string, McpServerDef>;
}
async function buildMcpServers(workDir: string): Promise<Record<string, McpServerDef>> {
const servers: Record<string, McpServerDef> = {};
// ── Gitea: only if credential exists ──
const giteaCreds = await getRawCredentialsByProvider("gitea");
if (giteaCreds.length > 0) {
const cred = giteaCreds[0];
servers.gitea = {
command: "gitea-mcp",
args: ["stdio"],
env: {
GITEA_TOKEN: cred.token,
GITEA_URL: cred.baseUrl || DEFAULT_GITEA_URL,
},
};
}
// ── Kubernetes: always (uses in-cluster SA token) ──
servers.kubernetes = {
command: "kubernetes-mcp-server",
args: [],
};
// ── Postgres: only if credential exists ──
const pgCreds = await getRawCredentialsByProvider("postgres-mcp");
if (pgCreds.length > 0) {
servers.postgres = {
command: "mcp-server-postgres",
args: [pgCreds[0].token], // token holds the connection string
};
}
// ── Filesystem: always (scoped to worktree) ──
servers.filesystem = {
command: "mcp-server-filesystem",
args: [workDir],
};
// ── Git: always (scoped to worktree) ──
servers.git = {
command: "mcp-server-git",
args: ["--repository", workDir],
};
return servers;
}
/**
* Generate .claude/settings.json in the worktree with MCP server configs.
* Only applies to claude-code runtime; other runtimes are a no-op.
*/
export async function generateMcpConfig(workDir: string, runtime: AgentRuntime): Promise<void> {
if (runtime !== "claude-code") return;
const mcpServers = await buildMcpServers(workDir);
const serverNames = Object.keys(mcpServers);
const settings: ClaudeProjectSettings = {
permissions: {
allow: serverNames.map((name) => `mcp__${name}__*`),
deny: [],
},
mcpServers,
};
const claudeDir = join(workDir, ".claude");
await mkdir(claudeDir, { recursive: true });
await writeFile(
join(claudeDir, "settings.json"),
JSON.stringify(settings, null, 2) + "\n",
);
}