Files
mcp-gitea-search/index.js
Peter Foster ddb530b673 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>
2025-12-17 02:24:16 +00:00

346 lines
8.7 KiB
JavaScript
Executable File

#!/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);