Files
gitea-mcp/Tools/SearchTools.cs
T
zhengchen.tao c7fa6aeb7f
Build Docker Image / build (push) Failing after 5m41s
Build Docker Image / deploy (push) Has been skipped
gitea-mcp: 初次落地 Gitea MCP Server (.NET 10, V1 only-read)
把 Gitea (git.zhengchentao.win) 通过 MCP 暴露给 Claude.ai:列 repo、读代码、看 commits / issues / PR / orgs / packages / actions。
设计文档见 vault Coding/gitea-mcp/gitea-mcp 设计.md。
代码模板复用 obsidian-mcp(.NET 10 + ModelContextProtocol SDK + JwtBearer)。

19 个只读 Tool(全部 scope=read:gitea):

Repo / 文件:
- list_repos / read_repo
- list_tree(max_entries=500 防爆)
- read_file(max_bytes=1MB,超出 truncated=true)
- search_code(走 /repos/search-code,indexer 未启用时返回结构化错误说明)

分支 / 提交:
- list_branches / list_commits / read_commit(diff 文件数限 50)

Issue / PR:
- list_issues / read_issue(含评论)
- list_pulls / read_pull(含评论 + 改动文件列表)

Org / Package(用户额外授权 read:organization + read:package):
- list_orgs / read_org
- list_packages / read_package

Gitea Actions(运维友好):
- list_workflow_runs / read_run_log

技术栈:
- .NET 10 + ModelContextProtocol SDK 1.0
- HttpClientFactory + Microsoft.Extensions.Http.Resilience(指数 backoff,5xx/429/网络错误重试)
- JwtBearer (HS256, Current+Previous fallback, MapInboundClaims=false)
- aud=gitea, scope=read:gitea, iss=https://auth.zhengchentao.win

Gitea API client:
- Authorization: token <PAT> (admin PAT,仅 read scope)
- BaseUrl=https://git.zhengchentao.win
- 错误映射:401/403 → UnauthorizedAccessException,404 → KeyNotFoundException,5xx → InvalidOperationException
- RepoBlacklist 黑名单(owner/repo 精确匹配,默认空)

部署:
- Dockerfile multi-stage,COPY --chown,non-root user
- .gitea/workflows/build-image.yml:build + deploy 双 job,buildkit v0.13.2
- 容器内 :8080,宿主端口 9092
- 子域名 git-mcp.zhengchentao.win(区别于 Gitea 本体 git.zhengchentao.win)

测试:6/6 单测过(GiteaRepoFilter 黑名单匹配)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 01:32:42 +08:00

76 lines
3.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using GiteaMcp.Services;
using GiteaMcp.Services.Models;
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace GiteaMcp.Tools;
/// <summary>代码搜索 Toolsearch_code</summary>
[McpServerToolType]
public class SearchTools(
GiteaApiClient gitea,
GiteaRepoFilter filter)
{
[McpServerTool]
[Description(
"Search code across Gitea repositories using Gitea's built-in code indexer. " +
"Requires Gitea to have REPO_INDEXER_ENABLED=true in app.ini. " +
"If the indexer is not enabled, returns an explanatory error instead of silently returning empty. " +
"Scope: when owner+repo are provided, searches only that repo; otherwise searches all accessible repos. " +
"Returns: owner, repo, file path, line number, and a preview snippet of the matching line. " +
"Limit: max 50 results. For large codebases, narrow with owner or repo.")]
public async Task<object> search_code(
[Description("Search query string. Supports keywords; no regex.")] string query,
[Description("Restrict search to this owner (user login or org name). Optional.")] string? owner = null,
[Description("Restrict search to this repo name (requires owner). Optional.")] string? repo = null,
[Description("Max number of results. Default 50.")] int? limit = null,
CancellationToken ct = default)
{
var lim = Math.Min(limit ?? 50, 50);
// 检查是否需要过滤黑名单
if (!string.IsNullOrWhiteSpace(owner) && !string.IsNullOrWhiteSpace(repo)
&& filter.IsBlocked($"{owner}/{repo}"))
{
throw new UnauthorizedAccessException($"Repo {owner}/{repo} is on the access blocklist.");
}
// 调 Gitea code search 端点(仅在 [indexer] REPO_INDEXER_ENABLED=true 时可用)。
// indexer 未启用 → API 返回 404GiteaApiClient.EnsureSuccessAsync 抛 KeyNotFoundException
// 这里捕获后返回结构化降级提示,避免 swallow 到空数组让 Claude 误以为"搜不到"。
try
{
var codeResult = await gitea.SearchCodeAsync(query, owner, repo, lim, ct);
var hits = codeResult.Data
.Where(d => d.Repo == null || !filter.IsBlocked(d.Repo.FullName))
.Take(lim)
.Select(d => (object)new
{
owner = d.Repo?.Owner?.Login ?? "",
repo = d.Repo?.Name ?? "",
path = d.Filename,
// Gitea code search 不返回精确行号,前端可在 preview 里自行定位
line = 0,
preview = d.Content?.Length > 200 ? d.Content[..200] + "..." : d.Content ?? "",
})
.ToList();
return new { ok = true, results = hits };
}
catch (KeyNotFoundException)
{
// indexer 未启用 → 404;返回结构化提示,不要 swallow 成空 results
return new
{
ok = false,
error = "indexer_disabled",
notice = "Gitea code search endpoint returned 404. " +
"Enable the code indexer by setting [indexer] REPO_INDEXER_ENABLED=true in app.ini and restart Gitea. " +
"Workaround: use list_tree + read_file to navigate files manually.",
results = Array.Empty<object>(),
};
}
}
}