diff --git a/packages/gitea-mcp/src/index.ts b/packages/gitea-mcp/src/index.ts index e430617..0df240c 100644 --- a/packages/gitea-mcp/src/index.ts +++ b/packages/gitea-mcp/src/index.ts @@ -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(); const runStatusCache = new Map(); // 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 = {}; + 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 = { 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}`); }); diff --git a/packages/memory-mcp/src/index.ts b/packages/memory-mcp/src/index.ts index 1251358..182514a 100644 --- a/packages/memory-mcp/src/index.ts +++ b/packages/memory-mcp/src/index.ts @@ -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) => {