3 Commits

Author SHA1 Message Date
Chris Farhood 03e2bc1e11 chore: remove unused MCP server from API package
CI / Type-check & lint (pull_request) Successful in 16s
CI / Build & push API image (pull_request) Has been skipped
CI / Build & push worker image (pull_request) Has been skipped
The MCP server was never wired into the API entry point — dead code.
The REST API + Paperclip skill provides sufficient surface area.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-19 10:26:41 +00:00
Chris Farhood ccb3dc6f75 Merge pull request 'chore: move .github folder to .gitea for Gitea compatibility' (#1) from far-133/move-github-to-gitea into main
CI / Type-check & lint (push) Successful in 17s
CI / Build & push API image (push) Successful in 59s
CI / Build & push worker image (push) Successful in 3m16s
Reviewed-on: #1
Reviewed-by: Chris Farhood <3+cpfarhood@noreply.git.farh.net>
2026-05-18 20:10:48 +00:00
Chris Farhood ff32ec85c5 chore: move .github folder to .gitea for Gitea compatibility
CI / Type-check & lint (pull_request) Successful in 15s
CI / Build & push worker image (pull_request) Has been skipped
CI / Build & push API image (pull_request) Has been skipped
Gitea prefers .gitea/ISSUE_TEMPLATE/ and .gitea/workflows/ over the
GitHub-convention .github/ equivalents. Moves all issue templates and
workflow files to the Gitea-native paths and updates CLAUDE.md references.

Cosign certificate identity paths in release/rollback workflows are
intentionally left unchanged — they reference the signing identity from
prior workflow runs and will need a separate update when the CI signing
infrastructure migrates.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-18 15:56:05 +00:00
8 changed files with 25 additions and 786 deletions
+24 -12
View File
@@ -7,16 +7,17 @@ on:
- 'charts/hightower/**'
permissions:
contents: read
packages: write
contents: write
jobs:
release:
name: Lint, package & push OCI
runs-on: ubuntu-latest
name: Lint, package & publish
runs-on: runners-farhoodlabs
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Install Helm
uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4.3.0
@@ -29,13 +30,24 @@ jobs:
mkdir -p .helm-packages
helm package charts/hightower -d .helm-packages
- name: Log in to Gitea OCI registry
run: |
echo "${{ secrets.REGISTRY_TOKEN }}" | helm registry login git.farh.net \
--username gitea-admin \
--password-stdin
- name: Checkout gh-pages
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: gh-pages
path: gh-pages
fetch-depth: 0
- name: Push chart to Gitea OCI registry
- name: Update Helm repo index
run: |
PACKAGE=$(ls .helm-packages/*.tgz | head -1)
helm push "$PACKAGE" oci://git.farh.net/farhoodlabs/charts
cp .helm-packages/*.tgz gh-pages/
helm repo index gh-pages --url https://farhoodlabs.github.io/hightower
- name: Push to gh-pages
run: |
cd gh-pages
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .
git diff --staged --quiet && echo "No changes to commit" && exit 0
git commit -m "Release Helm chart $(ls *.tgz | head -1)"
git push
+1 -1
View File
@@ -158,7 +158,7 @@ Comments must be **timeless** — no references to this conversation, refactorin
**Config:** `Dockerfile`, `apps/worker/configs/`, `apps/worker/prompts/`, `tsconfig.base.json` (shared compiler options), `turbo.json`, `biome.json`
**CI/CD:** `.github/workflows/ci.yml` (type-check, lint, build & push images to GHCR), `.github/workflows/release.yml` (Docker Hub push + GitHub release, manual dispatch)
**CI/CD:** `.gitea/workflows/ci.yml` (type-check, lint, build & push images to GHCR), `.gitea/workflows/release.yml` (Docker Hub push + GitHub release, manual dispatch)
## Package Installation
-1
View File
@@ -12,7 +12,6 @@
"dependencies": {
"@hono/node-server": "^1.14.0",
"@kubernetes/client-node": "^1.4.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@trebuchet/worker": "workspace:*",
"@temporalio/client": "^1.11.0",
"hono": "^4.7.0",
-2
View File
@@ -5,7 +5,6 @@
export interface Config {
readonly port: number;
readonly mcpPort: number;
readonly temporalAddress: string;
readonly apiKey: string;
readonly k8sNamespace: string;
@@ -29,7 +28,6 @@ export function loadConfig(): Config {
return {
port: Number(process.env.PORT) || 3000,
mcpPort: Number(process.env.MCP_PORT) || 3100,
temporalAddress: process.env.TEMPORAL_ADDRESS || 'hightower-temporal:7233',
apiKey,
k8sNamespace: process.env.K8S_NAMESPACE || 'hightower',
-204
View File
@@ -1,204 +0,0 @@
/**
* MCP server for Hightower scan management.
* Exposes scan-manager tools via the Model Context Protocol over HTTP.
*/
import http from 'node:http';
import type * as k8s from '@kubernetes/client-node';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import type { Client } from '@temporalio/client';
import { z } from 'zod';
import type { Config } from '../config.js';
import { cancelScan, getReport, getScan, listScans, startScan } from '../services/scan-manager.js';
import type { CreateScanInput } from '../types/api.js';
export interface McpServerDeps {
readonly config: Config;
readonly temporalClient: Client;
readonly batchApi: k8s.BatchV1Api;
readonly coreApi: k8s.CoreV1Api;
}
function createMcpServer(deps: McpServerDeps): McpServer {
const server = new McpServer(
{ name: 'hightower', version: '1.0.0' },
{
capabilities: {
tools: {},
},
},
);
// === Tool: start_scan ===
server.registerTool(
'start_scan',
{
description: 'Start a new penetration test scan. Returns the scan ID and initial status.',
inputSchema: z.object({
targetUrl: z.string().describe('Target URL to scan (e.g., https://example.com)'),
gitUrl: z.string().describe('Git URL of the repository to analyze (e.g., https://github.com/user/repo)'),
workspace: z
.string()
.optional()
.describe(
'Optional workspace name. Must match /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/. Defaults to auto-generated from target URL.',
),
gitRef: z.string().optional().describe('Optional Git branch/tag/commit to checkout before scanning.'),
pipelineTesting: z
.boolean()
.optional()
.describe('If true, runs in minimal testing mode with fast retries (10s). Use for development.'),
}),
},
async ({ targetUrl, gitUrl, workspace, gitRef, pipelineTesting }) => {
const input: CreateScanInput = {
targetUrl,
gitUrl,
workspace,
...(gitRef !== undefined && { gitRef }),
...(pipelineTesting !== undefined && { pipelineTesting }),
};
const result = await startScan(deps.config, deps.batchApi, input);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(result, null, 2),
},
],
};
},
);
// === Tool: get_scan ===
server.registerTool(
'get_scan',
{
description: 'Get the status, progress, and results of a running or completed scan.',
inputSchema: z.object({
scanId: z.string().describe('The scan ID returned from start_scan (e.g., hightower-worker-abc123)'),
}),
},
async ({ scanId }) => {
const result = await getScan(deps.config, deps.temporalClient, scanId);
if (!result) {
return {
content: [{ type: 'text' as const, text: `Scan '${scanId}' not found.` }],
isError: true,
};
}
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(result, null, 2),
},
],
};
},
);
// === Tool: list_scans ===
server.registerTool(
'list_scans',
{
description: 'List all running and historical scans.',
inputSchema: z.object({}),
},
async () => {
const results = await listScans(deps.config, deps.temporalClient, deps.batchApi);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(results, null, 2),
},
],
};
},
);
// === Tool: cancel_scan ===
server.registerTool(
'cancel_scan',
{
description: 'Cancel a running scan by terminating its Kubernetes Job and Temporal workflow.',
inputSchema: z.object({
scanId: z.string().describe('The scan ID to cancel.'),
}),
},
async ({ scanId }) => {
await cancelScan(deps.config, deps.temporalClient, deps.batchApi, scanId);
return {
content: [
{
type: 'text' as const,
text: `Scan '${scanId}' cancellation requested.`,
},
],
};
},
);
// === Tool: get_report ===
server.registerTool(
'get_report',
{
description: 'Get the final security report for a completed scan.',
inputSchema: z.object({
scanId: z.string().describe('The scan ID to get the report for.'),
}),
},
async ({ scanId }) => {
const report = await getReport(deps.config, scanId);
if (!report) {
return {
content: [
{
type: 'text' as const,
text: `Report for scan '${scanId}' not found.`,
},
],
isError: true,
};
}
return {
content: [{ type: 'text' as const, text: report }],
};
},
);
return server;
}
export async function startMcpServer(deps: McpServerDeps, port: number): Promise<http.Server> {
const mcpServer = createMcpServer(deps);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
});
// Cast to Transport — the SDK's Transport interface requires onclose: () => void
// but StreamableHTTPServerTransport allows undefined (handled internally).
await mcpServer.connect(transport as never);
const server = http.createServer((req, res) => {
transport.handleRequest(req, res, undefined);
});
return new Promise<http.Server>((resolve, reject) => {
server.on('error', reject);
server.listen(port, () => {
console.log(`MCP server listening on port ${port}`);
resolve(server);
});
});
}
-566
View File
File diff suppressed because it is too large Load Diff