Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
024b19064d |
No files matched your search
Binary file not shown.
Binary file not shown.
+20
-7
@@ -389,8 +389,9 @@ async def tea_label(repo: Repo, number: Number, add: Labels | None = None,
|
||||
|
||||
|
||||
@mcp.tool(description="Read a pull request: its branches, whether it can merge, the checks on its head, and its diff "
|
||||
"(first 200 KB).", annotations=READ)
|
||||
async def tea_pull_get(repo: Repo, number: Number, diff: bool = True) -> dict[str, Any]:
|
||||
"(first `diff_kb` KB, default 40; up to 200).", annotations=READ)
|
||||
async def tea_pull_get(repo: Repo, number: Number, diff: bool = True,
|
||||
diff_kb: Annotated[int, Field(ge=1, le=200)] = 40) -> dict[str, Any]:
|
||||
rp = repo_path(repo)
|
||||
p = await gitea("GET", f"{rp}/pulls/{number}")
|
||||
head, base = p.get("head") or {}, p.get("base") or {}
|
||||
@@ -402,7 +403,7 @@ async def tea_pull_get(repo: Repo, number: Number, diff: bool = True) -> dict[st
|
||||
"statuses": [{k: s.get(k) for k in ("context", "status", "description", "target_url")}
|
||||
for s in checks.get("statuses") or []]}}
|
||||
if diff:
|
||||
out["diff"], out["diff_truncated"] = await gitea_text(f"{rp}/pulls/{number}.diff", cap=MAX_BYTES)
|
||||
out["diff"], out["diff_truncated"] = await gitea_text(f"{rp}/pulls/{number}.diff", cap=diff_kb * 1000)
|
||||
return out
|
||||
|
||||
|
||||
@@ -537,9 +538,12 @@ async def tea_tree(repo: Repo, path: Annotated[str, Field(pattern=r"^[^\x00-\x1f
|
||||
"entries": [{k: e.get(k) for k in ("name", "type", "size")} for e in rows[:500]]}
|
||||
|
||||
|
||||
@mcp.tool(description="Read a text file from a repository (up to 200 KB). Larger and binary files report only their "
|
||||
"size and sha.", annotations=READ)
|
||||
async def tea_file_get(repo: Repo, path: FilePath, ref: Ref = "") -> dict[str, Any]:
|
||||
@mcp.tool(description="Read a text file from a repository, a window of lines at a time: `start` is the first line "
|
||||
"(1-based) and `lines` how many (default 400). The reply says the file's total line count and "
|
||||
"whether more follows, so read further only when you need it.", annotations=READ)
|
||||
async def tea_file_get(repo: Repo, path: FilePath, ref: Ref = "",
|
||||
start: Annotated[int, Field(ge=1)] = 1,
|
||||
lines: Annotated[int, Field(ge=1, le=2000)] = 400) -> dict[str, Any]:
|
||||
p = file_path(path)
|
||||
x = await gitea("GET", contents(repo_path(repo), p), params={"ref": ref} if ref else None)
|
||||
if not isinstance(x, dict) or x.get("type") != "file":
|
||||
@@ -548,9 +552,18 @@ async def tea_file_get(repo: Repo, path: FilePath, ref: Ref = "") -> dict[str, A
|
||||
if (x.get("size") or 0) > MAX_BYTES:
|
||||
return {**meta, "too_large": True}
|
||||
try:
|
||||
return {**meta, "content": base64.b64decode(x.get("content") or "").decode("utf-8")}
|
||||
text = base64.b64decode(x.get("content") or "").decode("utf-8")
|
||||
except (binascii.Error, ValueError):
|
||||
return {**meta, "binary": True}
|
||||
return {**meta, **line_window(text, start, lines)}
|
||||
|
||||
|
||||
def line_window(text: str, start: int, lines: int) -> dict[str, Any]:
|
||||
"""A window of a text: chat clients resend every tool result each turn, so whole files get expensive fast."""
|
||||
rows = text.splitlines(keepends=True)
|
||||
part = rows[start - 1:start - 1 + lines]
|
||||
end = start - 1 + len(part)
|
||||
return {"content": "".join(part), "start": start, "end": end, "total_lines": len(rows), "more": end < len(rows)}
|
||||
|
||||
|
||||
@mcp.tool(description="Create or update one text file on a branch, as a commit by you. Branch protection applies.",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -272,7 +272,7 @@ class TeaMCP(unittest.IsolatedAsyncioTestCase):
|
||||
pr = await self.call("tea_pull_get", repo="o/r", number=2)
|
||||
self.assertEqual((pr["head"], pr["base"], pr["head_sha"], pr["is_pull"]), ("feature", "trunk", "abcdef123456", True))
|
||||
self.assertEqual((pr["checks"]["state"], pr["checks"]["statuses"][0]["context"]), ("success", "ci / test"))
|
||||
self.assertEqual((len(pr["diff"]), pr["diff_truncated"]), (server.MAX_BYTES, True))
|
||||
self.assertEqual((len(pr["diff"]), pr["diff_truncated"]), (40_000, True))
|
||||
self.assertNotIn("diff", await self.call("tea_pull_get", repo="o/r", number=2, diff=False))
|
||||
review = await self.call("tea_pull_review", repo="o/r", number=2, event="APPROVED")
|
||||
self.assertEqual(review["state"], "APPROVED")
|
||||
@@ -295,6 +295,11 @@ class TeaMCP(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertIn(b"/api/v1/repos/o/r/contents/docs/a%20b.md", self.gitea.raw_paths)
|
||||
self.gitea.files["big.txt"] = b"x" * (server.MAX_BYTES + 1)
|
||||
self.assertTrue((await self.call("tea_file_get", repo="o/r", path="big.txt"))["too_large"])
|
||||
self.assertEqual({k: text[k] for k in ("start", "end", "total_lines", "more")},
|
||||
{"start": 1, "end": 1, "total_lines": 1, "more": False})
|
||||
from tea_mcp.server import line_window
|
||||
w = line_window("".join(f"{i}\n" for i in range(1, 11)), 4, 3)
|
||||
self.assertEqual((w["content"], w["end"], w["more"]), ("4\n5\n6\n", 6, True))
|
||||
self.assertIn("not a file", await self.refused("tea_file_get", repo="o/r", path="docs"))
|
||||
self.assertIn("plain repository path", await self.refused("tea_file_get", repo="o/r", path="docs/../x"))
|
||||
|
||||
|
||||
Reference in new issue
Block a user