Add MCP servers (Gitea, K8s, Postgres, filesystem, git) to harness agents
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:
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
93
apps/harness/src/lib/mcp-config.ts
Normal file
93
apps/harness/src/lib/mcp-config.ts
Normal 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",
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user