1
0

feat: replace custom Gitea client with MCP servers

Replace the bespoke GiteaHand httpx client and tools_gitea.py wrappers
with official MCP tool servers (gitea-mcp + filesystem MCP), wired into
Agno via MCPTools. Switch all session functions to async (arun/acontinue_run)
so MCP tools auto-connect. Delete ~1070 lines of custom Gitea code.

- Create src/timmy/mcp_tools.py with MCP factories + standalone issue bridge
- Wire MCPTools into agent.py tool list (Gitea + filesystem)
- Switch session.py chat/chat_with_tools/continue_chat to async
- Update all callers (dashboard routes, Discord vendor, CLI, thinking engine)
- Add gitea_token fallback from ~/.config/gitea/token
- Add MCP session cleanup to app shutdown hook
- Update tool_safety.py for MCP tool names
- 11 new tests, all 1417 passing, coverage 74.2%

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trip T
2026-03-12 21:40:32 -04:00
parent 41d6ebaf6a
commit 78167675f2
24 changed files with 664 additions and 1170 deletions

View File

@@ -1,6 +1,6 @@
"""Tests for dashboard tool confirmation flow using native Agno RunOutput."""
from unittest.mock import MagicMock, patch
from unittest.mock import AsyncMock, MagicMock, patch
def _mock_completed_run(content="Just a reply."):
@@ -47,7 +47,7 @@ def test_chat_with_tool_call_shows_approval_card(client):
item = _mock_approval_item()
with (
patch("dashboard.routes.agents.chat_with_tools", return_value=run),
patch("dashboard.routes.agents.chat_with_tools", new_callable=AsyncMock, return_value=run),
patch("timmy.approvals.create_item", return_value=item),
):
response = client.post("/agents/default/chat", data={"message": "run echo hello"})
@@ -76,7 +76,7 @@ def test_chat_tool_card_contains_impact_badge(client):
item = _mock_approval_item()
with (
patch("dashboard.routes.agents.chat_with_tools", return_value=run),
patch("dashboard.routes.agents.chat_with_tools", new_callable=AsyncMock, return_value=run),
patch("timmy.approvals.create_item", return_value=item),
):
response = client.post("/agents/default/chat", data={"message": "run it"})
@@ -90,7 +90,7 @@ def test_chat_tool_card_has_htmx_approve_endpoint(client):
item = _mock_approval_item()
with (
patch("dashboard.routes.agents.chat_with_tools", return_value=run),
patch("dashboard.routes.agents.chat_with_tools", new_callable=AsyncMock, return_value=run),
patch("timmy.approvals.create_item", return_value=item),
):
response = client.post("/agents/default/chat", data={"message": "run it"})
@@ -109,7 +109,7 @@ def _create_pending_tool(client, approval_id="test-approval-123"):
item = _mock_approval_item(approval_id)
with (
patch("dashboard.routes.agents.chat_with_tools", return_value=run),
patch("dashboard.routes.agents.chat_with_tools", new_callable=AsyncMock, return_value=run),
patch("timmy.approvals.create_item", return_value=item),
):
response = client.post("/agents/default/chat", data={"message": "run it"})
@@ -131,7 +131,9 @@ def test_approve_executes_tool_and_returns_result(client):
result_run.content = "Done."
with (
patch("dashboard.routes.agents.continue_chat", return_value=result_run),
patch(
"dashboard.routes.agents.continue_chat", new_callable=AsyncMock, return_value=result_run
),
patch("timmy.approvals.approve"),
):
response = client.post(f"/agents/default/tool/{approval_id}/approve")
@@ -153,7 +155,9 @@ def test_approve_same_id_twice_returns_404(client):
result_run = _mock_completed_run("ok")
with (
patch("dashboard.routes.agents.continue_chat", return_value=result_run),
patch(
"dashboard.routes.agents.continue_chat", new_callable=AsyncMock, return_value=result_run
),
patch("timmy.approvals.approve"),
):
client.post(f"/agents/default/tool/{approval_id}/approve")
@@ -171,7 +175,11 @@ def test_reject_returns_rejected_card(client):
approval_id = _create_pending_tool(client)
with (
patch("dashboard.routes.agents.continue_chat", return_value=_mock_completed_run()),
patch(
"dashboard.routes.agents.continue_chat",
new_callable=AsyncMock,
return_value=_mock_completed_run(),
),
patch("timmy.approvals.reject"),
):
response = client.post(f"/agents/default/tool/{approval_id}/reject")

View File

@@ -104,7 +104,9 @@ def _mock_run(content="Operational and ready."):
def test_chat_agent_success(client):
with patch("dashboard.routes.agents.chat_with_tools", return_value=_mock_run()):
with patch(
"dashboard.routes.agents.chat_with_tools", new_callable=AsyncMock, return_value=_mock_run()
):
response = client.post("/agents/default/chat", data={"message": "status?"})
assert response.status_code == 200
@@ -113,7 +115,11 @@ def test_chat_agent_success(client):
def test_chat_agent_shows_user_message(client):
with patch("dashboard.routes.agents.chat_with_tools", return_value=_mock_run("Acknowledged.")):
with patch(
"dashboard.routes.agents.chat_with_tools",
new_callable=AsyncMock,
return_value=_mock_run("Acknowledged."),
):
response = client.post("/agents/default/chat", data={"message": "hello there"})
assert "hello there" in response.text
@@ -123,6 +129,7 @@ def test_chat_agent_ollama_offline(client):
# When Ollama is unreachable, chat shows the user message + error.
with patch(
"dashboard.routes.agents.chat_with_tools",
new_callable=AsyncMock,
side_effect=Exception("Ollama unreachable"),
):
response = client.post("/agents/default/chat", data={"message": "ping"})
@@ -147,7 +154,9 @@ def test_history_empty_shows_init_message(client):
def test_history_records_user_and_agent_messages(client):
with patch(
"dashboard.routes.agents.chat_with_tools", return_value=_mock_run("I am operational.")
"dashboard.routes.agents.chat_with_tools",
new_callable=AsyncMock,
return_value=_mock_run("I am operational."),
):
client.post("/agents/default/chat", data={"message": "status check"})
@@ -158,6 +167,7 @@ def test_history_records_user_and_agent_messages(client):
def test_history_records_error_when_offline(client):
with patch(
"dashboard.routes.agents.chat_with_tools",
new_callable=AsyncMock,
side_effect=Exception("Ollama unreachable"),
):
client.post("/agents/default/chat", data={"message": "ping"})
@@ -167,7 +177,11 @@ def test_history_records_error_when_offline(client):
def test_history_clear_resets_to_init_message(client):
with patch("dashboard.routes.agents.chat_with_tools", return_value=_mock_run("Acknowledged.")):
with patch(
"dashboard.routes.agents.chat_with_tools",
new_callable=AsyncMock,
return_value=_mock_run("Acknowledged."),
):
client.post("/agents/default/chat", data={"message": "hello"})
response = client.delete("/agents/default/history")
@@ -176,7 +190,11 @@ def test_history_clear_resets_to_init_message(client):
def test_history_empty_after_clear(client):
with patch("dashboard.routes.agents.chat_with_tools", return_value=_mock_run("OK.")):
with patch(
"dashboard.routes.agents.chat_with_tools",
new_callable=AsyncMock,
return_value=_mock_run("OK."),
):
client.post("/agents/default/chat", data={"message": "test"})
client.delete("/agents/default/history")