feat(mcps): enhance Gitea MCP with new tools and fix Memory MCP stdio execution
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 8s
Monorepo Pipeline / 🧹 Lint (push) Failing after 12s
Monorepo Pipeline / 🧪 Test (push) Failing after 13s
Monorepo Pipeline / 🏗️ Build (push) Failing after 13s
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:
2026-03-04 15:20:15 +01:00
parent 0c27e3b5d8
commit 4430d473cb
2 changed files with 287 additions and 18 deletions

View File

@@ -58,6 +58,128 @@ const GET_PIPELINE_LOGS_TOOL: Tool = {
},
};
const WAIT_PIPELINE_COMPLETION_TOOL: Tool = {
name: "gitea_wait_pipeline_completion",
description: "BLOCKS and waits until a pipeline run completes, fails, or is cancelled. Use this instead of polling manually to save tokens.",
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" },
timeout_minutes: { type: "number", description: "Maximum time to wait before aborting (default: 10)" },
},
required: ["owner", "repo", "run_id"],
},
};
const LIST_ISSUES_TOOL: Tool = {
name: "gitea_list_issues",
description: "List issues for a repository",
inputSchema: {
type: "object",
properties: {
owner: { type: "string", description: "Repository owner" },
repo: { type: "string", description: "Repository name" },
state: { type: "string", description: "Filter by state: open, closed, or all (default: open)" },
limit: { type: "number", description: "Number of issues to fetch (default: 10)" },
},
required: ["owner", "repo"],
},
};
const CREATE_ISSUE_TOOL: Tool = {
name: "gitea_create_issue",
description: "Create a new issue in a repository",
inputSchema: {
type: "object",
properties: {
owner: { type: "string", description: "Repository owner" },
repo: { type: "string", description: "Repository name" },
title: { type: "string", description: "Issue title" },
body: { type: "string", description: "Issue description/body" },
},
required: ["owner", "repo", "title"],
},
};
const GET_FILE_CONTENT_TOOL: Tool = {
name: "gitea_get_file_content",
description: "Get the raw content of a file from a repository",
inputSchema: {
type: "object",
properties: {
owner: { type: "string", description: "Repository owner" },
repo: { type: "string", description: "Repository name" },
filepath: { type: "string", description: "Path to the file in the repository" },
ref: { type: "string", description: "The name of the commit/branch/tag (default: main)" },
},
required: ["owner", "repo", "filepath"],
},
};
const UPDATE_ISSUE_TOOL: Tool = {
name: "gitea_update_issue",
description: "Update an existing issue (e.g. change state, title, or body)",
inputSchema: {
type: "object",
properties: {
owner: { type: "string", description: "Repository owner" },
repo: { type: "string", description: "Repository name" },
index: { type: "number", description: "Issue index/number" },
state: { type: "string", description: "Optional: 'open' or 'closed'" },
title: { type: "string", description: "Optional: New title" },
body: { type: "string", description: "Optional: New body text" },
},
required: ["owner", "repo", "index"],
},
};
const CREATE_ISSUE_COMMENT_TOOL: Tool = {
name: "gitea_create_issue_comment",
description: "Add a comment to an existing issue or pull request",
inputSchema: {
type: "object",
properties: {
owner: { type: "string", description: "Repository owner" },
repo: { type: "string", description: "Repository name" },
index: { type: "number", description: "Issue or PR index/number" },
body: { type: "string", description: "Comment body text" },
},
required: ["owner", "repo", "index", "body"],
},
};
const CREATE_PULL_REQUEST_TOOL: Tool = {
name: "gitea_create_pull_request",
description: "Create a new Pull Request",
inputSchema: {
type: "object",
properties: {
owner: { type: "string", description: "Repository owner" },
repo: { type: "string", description: "Repository name" },
head: { type: "string", description: "The branch you want to merge (e.g., 'feature/my-changes')" },
base: { type: "string", description: "The branch to merge into (e.g., 'main')" },
title: { type: "string", description: "PR title" },
body: { type: "string", description: "Optional: PR description" },
},
required: ["owner", "repo", "head", "base", "title"],
},
};
const SEARCH_REPOS_TOOL: Tool = {
name: "gitea_search_repos",
description: "Search for repositories accessible to the authenticated user",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search term" },
limit: { type: "number", description: "Maximum number of results (default: 10)" },
},
required: ["query"],
},
};
// Subscription State
const subscriptions = new Set<string>();
const runStatusCache = new Map<string, string>(); // uri -> status
@@ -78,7 +200,18 @@ const server = new Server(
// --- Tools ---
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [LIST_PIPELINES_TOOL, GET_PIPELINE_LOGS_TOOL],
tools: [
LIST_PIPELINES_TOOL,
GET_PIPELINE_LOGS_TOOL,
WAIT_PIPELINE_COMPLETION_TOOL,
LIST_ISSUES_TOOL,
CREATE_ISSUE_TOOL,
GET_FILE_CONTENT_TOOL,
UPDATE_ISSUE_TOOL,
CREATE_ISSUE_COMMENT_TOOL,
CREATE_PULL_REQUEST_TOOL,
SEARCH_REPOS_TOOL
],
};
});
@@ -150,6 +283,133 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}
}
if (request.params.name === "gitea_wait_pipeline_completion") {
const { owner, repo, run_id, timeout_minutes = 10 } = request.params.arguments as any;
const startTime = Date.now();
const timeoutMs = timeout_minutes * 60 * 1000;
try {
while (true) {
if (Date.now() - startTime > timeoutMs) {
return { content: [{ type: "text", text: `Wait timed out after ${timeout_minutes} minutes.` }] };
}
const response = await giteaClient.get(`/repos/${owner}/${repo}/actions/runs/${run_id}`);
const status = response.data.status;
const conclusion = response.data.conclusion;
if (status !== "running" && status !== "waiting") {
return {
content: [{
type: "text",
text: `Pipeline finished! Final Status: ${status}, Conclusion: ${conclusion}`
}]
};
}
// Wait 5 seconds before polling again
await new Promise(resolve => setTimeout(resolve, 5000));
}
} catch (error: any) {
return { isError: true, content: [{ type: "text", text: `Error checking pipeline status: ${error.message}` }] };
}
}
if (request.params.name === "gitea_list_issues") {
const { owner, repo, state = "open", limit = 10 } = request.params.arguments as any;
try {
const response = await giteaClient.get(`/repos/${owner}/${repo}/issues`, {
params: { state, limit }
});
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
} catch (error: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${error.message}` }] };
}
}
if (request.params.name === "gitea_create_issue") {
const { owner, repo, title, body } = request.params.arguments as any;
try {
const response = await giteaClient.post(`/repos/${owner}/${repo}/issues`, {
title,
body
});
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
} catch (error: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${error.message}` }] };
}
}
if (request.params.name === "gitea_get_file_content") {
const { owner, repo, filepath, ref = "main" } = request.params.arguments as any;
try {
const response = await giteaClient.get(`/repos/${owner}/${repo}/contents/${filepath}`, {
params: { ref }
});
// Gitea returns base64 encoded content for files
if (response.data.type === 'file' && response.data.content) {
const decodedContent = Buffer.from(response.data.content, 'base64').toString('utf-8');
return { content: [{ type: "text", text: decodedContent }] };
}
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
} catch (error: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${error.message}` }] };
}
}
if (request.params.name === "gitea_update_issue") {
const { owner, repo, index, state, title, body } = request.params.arguments as any;
try {
const updateData: Record<string, any> = {};
if (state) updateData.state = state;
if (title) updateData.title = title;
if (body) updateData.body = body;
// Send PATCH request to /repos/{owner}/{repo}/issues/{index}
const response = await giteaClient.patch(`/repos/${owner}/${repo}/issues/${index}`, updateData);
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
} catch (error: any) {
return { isError: true, content: [{ type: "text", text: `Error updating issue: ${error.message}` }] };
}
}
if (request.params.name === "gitea_create_issue_comment") {
const { owner, repo, index, body } = request.params.arguments as any;
try {
const response = await giteaClient.post(`/repos/${owner}/${repo}/issues/${index}/comments`, {
body
});
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
} catch (error: any) {
return { isError: true, content: [{ type: "text", text: `Error creating comment: ${error.message}` }] };
}
}
if (request.params.name === "gitea_create_pull_request") {
const { owner, repo, head, base, title, body } = request.params.arguments as any;
try {
const prData: Record<string, any> = { head, base, title };
if (body) prData.body = body;
const response = await giteaClient.post(`/repos/${owner}/${repo}/pulls`, prData);
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
} catch (error: any) {
return { isError: true, content: [{ type: "text", text: `Error creating Pull Request: ${error.message}` }] };
}
}
if (request.params.name === "gitea_search_repos") {
const { query, limit = 10 } = request.params.arguments as any;
try {
const response = await giteaClient.get(`/repos/search`, {
params: { q: query, limit }
});
return { content: [{ type: "text", text: JSON.stringify(response.data.data, null, 2) }] };
} catch (error: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${error.message}` }] };
}
}
throw new Error(`Unknown tool: ${request.params.name}`);
});

View File

@@ -68,27 +68,36 @@ async function main() {
}
);
const app = express();
let transport: SSEServerTransport | null = null;
const isStdio = process.argv.includes('--stdio');
app.get('/sse', async (req, res) => {
console.error('New SSE connection established');
transport = new SSEServerTransport('/message', res);
if (isStdio) {
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
const transport = new StdioServerTransport();
await server.connect(transport);
});
console.error('Memory MCP server is running on stdio');
} else {
const app = express();
let transport: SSEServerTransport | null = null;
app.post('/message', async (req, res) => {
if (!transport) {
res.status(400).send('No active SSE connection');
return;
}
await transport.handlePostMessage(req, res);
});
app.get('/sse', async (req, res) => {
console.error('New SSE connection established');
transport = new SSEServerTransport('/message', res);
await server.connect(transport);
});
const PORT = process.env.MEMORY_MCP_PORT || 3002;
app.listen(PORT, () => {
console.error(`Memory MCP server is running on http://localhost:${PORT}/sse`);
});
app.post('/message', async (req, res) => {
if (!transport) {
res.status(400).send('No active SSE connection');
return;
}
await transport.handlePostMessage(req, res);
});
const PORT = process.env.MEMORY_MCP_PORT || 3002;
app.listen(PORT, () => {
console.error(`Memory MCP server is running on http://localhost:${PORT}/sse`);
});
}
}
main().catch((error) => {