Initial commit: MCP server for Gitea repository search
Provides tools for searching files and content across all Gitea repositories: - gitea_list_repos, gitea_search_files, gitea_search_content - gitea_get_file, gitea_repo_info, gitea_list_files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
345
index.js
Executable file
345
index.js
Executable file
@@ -0,0 +1,345 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
const GITEA_URL = process.env.GITEA_URL || "http://pfoster.dynu.net:3000";
|
||||
const GITEA_TOKEN = process.env.GITEA_TOKEN || "";
|
||||
|
||||
async function apiGet(endpoint) {
|
||||
const headers = {};
|
||||
if (GITEA_TOKEN) {
|
||||
headers["Authorization"] = `token ${GITEA_TOKEN}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${GITEA_URL}/api/v1${endpoint}`, { headers });
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function listRepos() {
|
||||
const allRepos = [];
|
||||
let page = 1;
|
||||
|
||||
while (page < 20) {
|
||||
const data = await apiGet(`/repos/search?limit=50&page=${page}`);
|
||||
if (!data.data || data.data.length === 0) break;
|
||||
allRepos.push(...data.data);
|
||||
page++;
|
||||
}
|
||||
|
||||
return allRepos;
|
||||
}
|
||||
|
||||
async function searchFilesByName(pattern) {
|
||||
const repos = await listRepos();
|
||||
const results = [];
|
||||
|
||||
for (const repo of repos) {
|
||||
try {
|
||||
const tree = await apiGet(
|
||||
`/repos/${repo.owner.login}/${repo.name}/git/trees/${repo.default_branch}?recursive=true`
|
||||
);
|
||||
|
||||
if (tree.tree) {
|
||||
const regex = new RegExp(pattern, "i");
|
||||
const matches = tree.tree
|
||||
.filter((f) => regex.test(f.path))
|
||||
.map((f) => ({
|
||||
repo: `${repo.owner.login}/${repo.name}`,
|
||||
path: f.path,
|
||||
type: f.type,
|
||||
url: `${GITEA_URL}/${repo.owner.login}/${repo.name}/src/branch/${repo.default_branch}/${f.path}`,
|
||||
}));
|
||||
|
||||
results.push(...matches);
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip repos we can't access
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function searchFileContents(query) {
|
||||
const repos = await listRepos();
|
||||
const results = [];
|
||||
|
||||
for (const repo of repos) {
|
||||
try {
|
||||
// Use Gitea's code search API
|
||||
const data = await apiGet(
|
||||
`/repos/${repo.owner.login}/${repo.name}/search?q=${encodeURIComponent(query)}`
|
||||
);
|
||||
|
||||
if (data && Array.isArray(data) && data.length > 0) {
|
||||
results.push({
|
||||
repo: `${repo.owner.login}/${repo.name}`,
|
||||
matches: data.map((m) => ({
|
||||
path: m.path,
|
||||
lineNumber: m.line_number,
|
||||
content: m.content?.substring(0, 200),
|
||||
})),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip repos without code search or access
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function getFileContent(repoFullName, filePath) {
|
||||
const [owner, repo] = repoFullName.split("/");
|
||||
const data = await apiGet(
|
||||
`/repos/${owner}/${repo}/contents/${encodeURIComponent(filePath)}`
|
||||
);
|
||||
|
||||
if (data.content) {
|
||||
return Buffer.from(data.content, "base64").toString("utf-8");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function getRepoInfo(repoFullName) {
|
||||
const [owner, repo] = repoFullName.split("/");
|
||||
return await apiGet(`/repos/${owner}/${repo}`);
|
||||
}
|
||||
|
||||
// Create MCP server
|
||||
const server = new Server(
|
||||
{
|
||||
name: "mcp-gitea-search",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// List available tools
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: "gitea_list_repos",
|
||||
description: "List all accessible repositories on the Gitea server",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gitea_search_files",
|
||||
description:
|
||||
"Search for files by name pattern (regex) across all repositories",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
pattern: {
|
||||
type: "string",
|
||||
description:
|
||||
"Regex pattern to match file names (e.g., '\\.csproj$' for .csproj files, 'appsettings' for files containing appsettings)",
|
||||
},
|
||||
},
|
||||
required: ["pattern"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gitea_search_content",
|
||||
description:
|
||||
"Search for content within files across all repositories (requires Gitea code indexer)",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
description: "Text to search for in file contents",
|
||||
},
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gitea_get_file",
|
||||
description: "Get the contents of a specific file from a repository",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
repo: {
|
||||
type: "string",
|
||||
description: "Repository full name (e.g., 'owner/repo')",
|
||||
},
|
||||
path: {
|
||||
type: "string",
|
||||
description: "File path within the repository",
|
||||
},
|
||||
},
|
||||
required: ["repo", "path"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gitea_repo_info",
|
||||
description: "Get information about a specific repository",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
repo: {
|
||||
type: "string",
|
||||
description: "Repository full name (e.g., 'owner/repo')",
|
||||
},
|
||||
},
|
||||
required: ["repo"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gitea_list_files",
|
||||
description: "List all files in a repository",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
repo: {
|
||||
type: "string",
|
||||
description: "Repository full name (e.g., 'owner/repo')",
|
||||
},
|
||||
},
|
||||
required: ["repo"],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// Handle tool calls
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
try {
|
||||
switch (name) {
|
||||
case "gitea_list_repos": {
|
||||
const repos = await listRepos();
|
||||
const summary = repos.map((r) => ({
|
||||
name: `${r.owner.login}/${r.name}`,
|
||||
description: r.description || "(no description)",
|
||||
stars: r.stars_count,
|
||||
updated: r.updated_at,
|
||||
}));
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(summary, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "gitea_search_files": {
|
||||
const results = await searchFilesByName(args.pattern);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
results.length > 0
|
||||
? JSON.stringify(results, null, 2)
|
||||
: `No files found matching pattern: ${args.pattern}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "gitea_search_content": {
|
||||
const results = await searchFileContents(args.query);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
results.length > 0
|
||||
? JSON.stringify(results, null, 2)
|
||||
: `No content found matching: ${args.query}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "gitea_get_file": {
|
||||
const content = await getFileContent(args.repo, args.path);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: content || "File not found or empty",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "gitea_repo_info": {
|
||||
const info = await getRepoInfo(args.repo);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(info, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "gitea_list_files": {
|
||||
const [owner, repo] = args.repo.split("/");
|
||||
const repoInfo = await apiGet(`/repos/${owner}/${repo}`);
|
||||
const tree = await apiGet(
|
||||
`/repos/${owner}/${repo}/git/trees/${repoInfo.default_branch}?recursive=true`
|
||||
);
|
||||
const files = tree.tree
|
||||
?.filter((f) => f.type === "blob")
|
||||
.map((f) => f.path);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(files, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Start server
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("Gitea MCP server running on stdio");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
Reference in New Issue
Block a user