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
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:
@@ -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
|
// Subscription State
|
||||||
const subscriptions = new Set<string>();
|
const subscriptions = new Set<string>();
|
||||||
const runStatusCache = new Map<string, string>(); // uri -> status
|
const runStatusCache = new Map<string, string>(); // uri -> status
|
||||||
@@ -78,7 +200,18 @@ const server = new Server(
|
|||||||
// --- Tools ---
|
// --- Tools ---
|
||||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
return {
|
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}`);
|
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -68,27 +68,36 @@ async function main() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const app = express();
|
const isStdio = process.argv.includes('--stdio');
|
||||||
let transport: SSEServerTransport | null = null;
|
|
||||||
|
|
||||||
app.get('/sse', async (req, res) => {
|
if (isStdio) {
|
||||||
console.error('New SSE connection established');
|
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
||||||
transport = new SSEServerTransport('/message', res);
|
const transport = new StdioServerTransport();
|
||||||
await server.connect(transport);
|
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) => {
|
app.get('/sse', async (req, res) => {
|
||||||
if (!transport) {
|
console.error('New SSE connection established');
|
||||||
res.status(400).send('No active SSE connection');
|
transport = new SSEServerTransport('/message', res);
|
||||||
return;
|
await server.connect(transport);
|
||||||
}
|
});
|
||||||
await transport.handlePostMessage(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
const PORT = process.env.MEMORY_MCP_PORT || 3002;
|
app.post('/message', async (req, res) => {
|
||||||
app.listen(PORT, () => {
|
if (!transport) {
|
||||||
console.error(`Memory MCP server is running on http://localhost:${PORT}/sse`);
|
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) => {
|
main().catch((error) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user