feat(gitea-mcp): add custom Gitea MCP server for Antigravity compatibility
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🧪 Test (push) Failing after 50s
Monorepo Pipeline / 🧹 Lint (push) Failing after 3m6s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m13s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🧪 Test (push) Failing after 50s
Monorepo Pipeline / 🧹 Lint (push) Failing after 3m6s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m13s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
This commit is contained in:
19
packages/gitea-mcp/Dockerfile
Normal file
19
packages/gitea-mcp/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json pnpm-lock.yaml* ./
|
||||
RUN corepack enable pnpm && pnpm install
|
||||
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
RUN pnpm build
|
||||
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/package.json ./
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# Use node to run the compiled index.js
|
||||
ENTRYPOINT ["node", "dist/index.js"]
|
||||
20
packages/gitea-mcp/package.json
Normal file
20
packages/gitea-mcp/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@mintel/gitea-mcp",
|
||||
"version": "1.0.0",
|
||||
"description": "Native Gitea MCP server for 100% Antigravity compatibility",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.5.0",
|
||||
"zod": "^3.23.8",
|
||||
"axios": "^1.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3",
|
||||
"@types/node": "^20.14.10"
|
||||
}
|
||||
}
|
||||
265
packages/gitea-mcp/src/index.ts
Normal file
265
packages/gitea-mcp/src/index.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
ReadResourceRequestSchema,
|
||||
SubscribeRequestSchema,
|
||||
UnsubscribeRequestSchema,
|
||||
Tool,
|
||||
Resource,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { z } from "zod";
|
||||
import axios from "axios";
|
||||
|
||||
const GITEA_HOST = process.env.GITEA_HOST || "https://git.infra.mintel.me";
|
||||
const GITEA_ACCESS_TOKEN = process.env.GITEA_ACCESS_TOKEN;
|
||||
|
||||
if (!GITEA_ACCESS_TOKEN) {
|
||||
console.error("Error: GITEA_ACCESS_TOKEN environment variable is required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const giteaClient = axios.create({
|
||||
baseURL: `${GITEA_HOST.replace(/\/$/, '')}/api/v1`,
|
||||
headers: {
|
||||
Authorization: `token ${GITEA_ACCESS_TOKEN}`,
|
||||
},
|
||||
});
|
||||
|
||||
const LIST_PIPELINES_TOOL: Tool = {
|
||||
name: "gitea_list_pipelines",
|
||||
description: "List recent action runs (pipelines) for a specific repository",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
owner: { type: "string", description: "Repository owner (e.g., 'mmintel')" },
|
||||
repo: { type: "string", description: "Repository name (e.g., 'at-mintel')" },
|
||||
limit: { type: "number", description: "Number of runs to fetch (default: 5)" },
|
||||
},
|
||||
required: ["owner", "repo"],
|
||||
},
|
||||
};
|
||||
|
||||
const GET_PIPELINE_LOGS_TOOL: Tool = {
|
||||
name: "gitea_get_pipeline_logs",
|
||||
description: "Get detailed logs for a specific pipeline run or job",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
owner: { type: "string", description: "Repository owner" },
|
||||
repo: { type: "string", description: "Repository name" },
|
||||
run_id: { type: "number", description: "ID of the action run" },
|
||||
},
|
||||
required: ["owner", "repo", "run_id"],
|
||||
},
|
||||
};
|
||||
|
||||
// Subscription State
|
||||
const subscriptions = new Set<string>();
|
||||
const runStatusCache = new Map<string, string>(); // uri -> status
|
||||
|
||||
const server = new Server(
|
||||
{
|
||||
name: "gitea-mcp-native",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: { subscribe: true }, // Enable subscriptions
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// --- Tools ---
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [LIST_PIPELINES_TOOL, GET_PIPELINE_LOGS_TOOL],
|
||||
};
|
||||
});
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
if (request.params.name === "gitea_list_pipelines") {
|
||||
// ... (Keeping exact same implementation as before for brevity)
|
||||
const { owner, repo, limit = 5 } = request.params.arguments as any;
|
||||
|
||||
try {
|
||||
const runsResponse = await giteaClient.get(`/repos/${owner}/${repo}/actions/runs`, {
|
||||
params: { limit },
|
||||
});
|
||||
|
||||
const runs = runsResponse.data;
|
||||
const enhancedRuns = await Promise.all(
|
||||
runs.map(async (run: any) => {
|
||||
try {
|
||||
const jobsResponse = await giteaClient.get(`/repos/${owner}/${repo}/actions/runs/${run.id}/jobs`);
|
||||
return {
|
||||
id: run.id,
|
||||
name: run.name,
|
||||
status: run.status,
|
||||
created_at: run.created_at,
|
||||
jobs: jobsResponse.data.map((job: any) => ({
|
||||
id: job.id,
|
||||
name: job.name,
|
||||
status: job.status,
|
||||
conclusion: job.conclusion
|
||||
}))
|
||||
};
|
||||
} catch (e) {
|
||||
return { id: run.id, name: run.name, status: run.status, created_at: run.created_at, jobs: [] };
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(enhancedRuns, null, 2) }],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return { isError: true, content: [{ type: "text", text: `Error fetching pipelines: ${error.message}` }] };
|
||||
}
|
||||
}
|
||||
|
||||
if (request.params.name === "gitea_get_pipeline_logs") {
|
||||
const { owner, repo, run_id } = request.params.arguments as any;
|
||||
try {
|
||||
const jobsResponse = await giteaClient.get(`/repos/${owner}/${repo}/actions/runs/${run_id}/jobs`);
|
||||
const jobs = jobsResponse.data;
|
||||
const logs = jobs.map((job: any) => ({
|
||||
job_id: job.id,
|
||||
job_name: job.name,
|
||||
status: job.status,
|
||||
conclusion: job.conclusion,
|
||||
steps: job.steps.map((step: any) => ({
|
||||
name: step.name,
|
||||
status: step.status,
|
||||
conclusion: step.conclusion
|
||||
}))
|
||||
}));
|
||||
|
||||
return { content: [{ type: "text", text: JSON.stringify(logs, null, 2) }] };
|
||||
} catch (error: any) {
|
||||
return { isError: true, content: [{ type: "text", text: `Error: ${error.message}` }] };
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||
});
|
||||
|
||||
// --- Resources & Subscriptions ---
|
||||
// We will expose a dynamic resource URI pattern: gitea://runs/{owner}/{repo}/{run_id}
|
||||
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||
return {
|
||||
resources: [
|
||||
{
|
||||
uri: "gitea://runs",
|
||||
name: "Gitea Pipeline Runs",
|
||||
description: "Dynamic resource for subscribing to pipeline runs. Format: gitea://runs/{owner}/{repo}/{run_id}",
|
||||
mimeType: "application/json",
|
||||
}
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||
const uri = request.params.uri;
|
||||
const match = uri.match(/^gitea:\/\/runs\/([^\/]+)\/([^\/]+)\/(\d+)$/);
|
||||
|
||||
if (!match) {
|
||||
throw new Error(`Invalid resource URI. Must be gitea://runs/{owner}/{repo}/{run_id}`);
|
||||
}
|
||||
|
||||
const [, owner, repo, run_id] = match;
|
||||
|
||||
try {
|
||||
const runResponse = await giteaClient.get(`/repos/${owner}/${repo}/actions/runs/${run_id}`);
|
||||
const jobsResponse = await giteaClient.get(`/repos/${owner}/${repo}/actions/runs/${run_id}/jobs`);
|
||||
|
||||
const resourceContent = {
|
||||
run: runResponse.data,
|
||||
jobs: jobsResponse.data
|
||||
};
|
||||
|
||||
// Update internal cache when read
|
||||
runStatusCache.set(uri, runResponse.data.status);
|
||||
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri,
|
||||
mimeType: "application/json",
|
||||
text: JSON.stringify(resourceContent, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to read Gitea resource: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
server.setRequestHandler(SubscribeRequestSchema, async (request) => {
|
||||
const uri = request.params.uri;
|
||||
if (!uri.startsWith("gitea://runs/")) {
|
||||
throw new Error("Only gitea://runs resources can be subscribed to");
|
||||
}
|
||||
subscriptions.add(uri);
|
||||
console.error(`Client subscribed to ${uri}`);
|
||||
return {};
|
||||
});
|
||||
|
||||
server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
|
||||
const uri = request.params.uri;
|
||||
subscriptions.delete(uri);
|
||||
console.error(`Client unsubscribed from ${uri}`);
|
||||
return {};
|
||||
});
|
||||
|
||||
// The server polling mechanism that pushes updates to subscribed clients
|
||||
async function pollSubscriptions() {
|
||||
for (const uri of subscriptions) {
|
||||
const match = uri.match(/^gitea:\/\/runs\/([^\/]+)\/([^\/]+)\/(\d+)$/);
|
||||
if (!match) continue;
|
||||
|
||||
const [, owner, repo, run_id] = match;
|
||||
try {
|
||||
const runResponse = await giteaClient.get(`/repos/${owner}/${repo}/actions/runs/${run_id}`);
|
||||
const currentStatus = runResponse.data.status;
|
||||
const prevStatus = runStatusCache.get(uri);
|
||||
|
||||
// If status changed (e.g. running -> completed), notify client
|
||||
if (prevStatus !== currentStatus) {
|
||||
runStatusCache.set(uri, currentStatus);
|
||||
|
||||
server.notification({
|
||||
method: "notifications/resources/updated",
|
||||
params: { uri }
|
||||
});
|
||||
console.error(`Pushed update for ${uri}: ${prevStatus} -> ${currentStatus}`);
|
||||
|
||||
// Auto-unsubscribe if completed/failed so we don't poll forever?
|
||||
// Let the client decide, or we can handle it here if requested.
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error polling subscription ${uri}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Poll every 5 seconds
|
||||
setTimeout(pollSubscriptions, 5000);
|
||||
}
|
||||
|
||||
|
||||
async function run() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("Gitea MCP Native Server running on stdio");
|
||||
|
||||
// Start the background poller
|
||||
pollSubscriptions();
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
console.error("Fatal error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
16
packages/gitea-mcp/tsconfig.json
Normal file
16
packages/gitea-mcp/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
489
pnpm-lock.yaml
generated
489
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user