Skip to content

Commit 4f5f7ec

Browse files
Gabrymi93claude
andcommitted
feat: public MCP server — read-only context resources + refresh tool
- mcp_server.py: FastMCP server con tre risorse (session_bootstrap, workspace_triage, topic_index) lette dal branch context via raw URL; tool refresh_context triggera workflow_dispatch CI - pyproject.toml: extra [mcp] con mcp>=1.0; entry point agent-context-mcp; mcp aggiunto anche a [dev] - tests/test_mcp_server.py: 6 test su risorse e tool (fetch, no-token, success, error) - README: sezione MCP server con configurazione Claude Code; roadmap aggiornato Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent bee7ab6 commit 4f5f7ec

4 files changed

Lines changed: 238 additions & 1 deletion

File tree

README.md

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,55 @@ Il builder non crasha mai per dati mancanti:
169169
| Path locale non è un repo git | `available: false, reason: not_git_repo` |
170170
| `--workspace-root` non impostato | `available: false, reason: local_disabled` |
171171

172+
## MCP Server
173+
174+
Il repo include un server MCP pubblico che espone i tre artifact come risorse leggibili dagli agenti senza richiedere esecuzione locale del builder.
175+
176+
### Installazione
177+
178+
```bash
179+
pip install -e ".[mcp]"
180+
```
181+
182+
### Avvio
183+
184+
```bash
185+
agent-context-mcp
186+
```
187+
188+
### Risorse esposte
189+
190+
| URI | Contenuto |
191+
|---|---|
192+
| `context://session_bootstrap` | `session_bootstrap.md` dal branch `context` |
193+
| `context://workspace_triage` | `workspace_triage.json` dal branch `context` |
194+
| `context://topic_index` | `topic_index.json` dal branch `context` |
195+
196+
### Tool
197+
198+
| Tool | Descrizione |
199+
|---|---|
200+
| `refresh_context` | Triggera un nuovo build CI (richiede `GITHUB_TOKEN` con scope `workflow`) |
201+
202+
### Configurazione Claude Code
203+
204+
```json
205+
{
206+
"mcpServers": {
207+
"dataciviclab-context": {
208+
"command": "agent-context-mcp",
209+
"env": {
210+
"GITHUB_TOKEN": "<token-opzionale-per-refresh>"
211+
}
212+
}
213+
}
214+
}
215+
```
216+
217+
Le risorse sono aggiornate automaticamente ogni 6 ore dalla CI. Il token è opzionale per la sola lettura; serve solo per `refresh_context`.
218+
219+
Le variabili d'ambiente `ACB_REPO` e `ACB_BRANCH` permettono di puntare a un fork o a un branch diverso.
220+
172221
## Sviluppo
173222

174223
```bash
@@ -180,7 +229,8 @@ ruff check .
180229
## Roadmap
181230

182231
- **P0** — MVP: bootstrap, triage, topic index, git collector, GitHub Actions ✓
183-
- **P1** — Schema JSON per stabilità output; integrazione MCP `dataciviclab-state`
232+
- **P0.5** — MCP server pubblico: risorse read-only + tool refresh ✓
233+
- **P1** — Schema JSON per stabilità output; integrazione `dataciviclab-state` MCP locale
184234
- **P2** — Script wrapper locale; fast path in `AGENTS.md`
185235

186236
## Licenza

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,20 @@ dependencies = [
3030
]
3131

3232
[project.optional-dependencies]
33+
mcp = [
34+
"mcp>=1.0",
35+
]
3336
dev = [
3437
"pytest>=7.0",
3538
"pytest-cov>=4.0",
3639
"ruff>=0.1",
3740
"mypy>=1.0",
41+
"mcp>=1.0",
3842
]
3943

4044
[project.scripts]
4145
agent-context = "agent_context_builder.cli:main"
46+
agent-context-mcp = "agent_context_builder.mcp_server:main"
4247

4348
[tool.hatch.build.targets.wheel]
4449
packages = ["src/agent_context_builder"]
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""Public MCP server — serves DataCivicLab context artifacts from the context branch.
2+
3+
Exposes three read-only resources built by the CI workflow:
4+
context://session_bootstrap — session_bootstrap.md
5+
context://workspace_triage — workspace_triage.json
6+
context://topic_index — topic_index.json
7+
8+
One optional tool:
9+
refresh_context — triggers a new CI build (requires GITHUB_TOKEN with workflow scope)
10+
11+
Configuration via environment variables:
12+
ACB_REPO GitHub repo (default: dataciviclab/agent-context-builder)
13+
ACB_BRANCH Branch where artifacts are published (default: context)
14+
GITHUB_TOKEN Required only for the refresh_context tool
15+
"""
16+
17+
import os
18+
19+
import requests
20+
from mcp.server.fastmcp import FastMCP
21+
22+
_REPO = os.environ.get("ACB_REPO", "dataciviclab/agent-context-builder")
23+
_BRANCH = os.environ.get("ACB_BRANCH", "context")
24+
_RAW_BASE = f"https://raw.githubusercontent.com/{_REPO}/{_BRANCH}"
25+
_API_BASE = f"https://api.github.com/repos/{_REPO}"
26+
27+
mcp = FastMCP(
28+
"dataciviclab-context",
29+
instructions=(
30+
"DataCivicLab context artifacts, generated from GitHub every 6 hours. "
31+
"Start with session_bootstrap for a quick orientation, then use "
32+
"workspace_triage for machine-readable state and topic_index for "
33+
"targeted exploration by topic."
34+
),
35+
)
36+
37+
38+
def _fetch(path: str) -> str:
39+
"""Fetch a file from the context branch."""
40+
token = os.environ.get("GITHUB_TOKEN")
41+
headers = {"Authorization": f"token {token}"} if token else {}
42+
response = requests.get(f"{_RAW_BASE}/{path}", headers=headers, timeout=10)
43+
response.raise_for_status()
44+
return response.text
45+
46+
47+
@mcp.resource("context://session_bootstrap")
48+
def session_bootstrap() -> str:
49+
"""Orientamento rapido: repo attivi, PR aperte, discussion, stato locale, topic."""
50+
return _fetch("session_bootstrap.md")
51+
52+
53+
@mcp.resource("context://workspace_triage")
54+
def workspace_triage() -> str:
55+
"""Triage machine-readable: PR, issue, discussion, stato git per repo, warning."""
56+
return _fetch("workspace_triage.json")
57+
58+
59+
@mcp.resource("context://topic_index")
60+
def topic_index() -> str:
61+
"""Lookup per topic: repo rilevanti, path, prossimo passo suggerito."""
62+
return _fetch("topic_index.json")
63+
64+
65+
@mcp.tool()
66+
def refresh_context() -> str:
67+
"""Triggera un nuovo build del contesto su CI.
68+
69+
Richiede GITHUB_TOKEN con scope workflow.
70+
Gli artifact aggiornati saranno disponibili entro ~1 minuto.
71+
"""
72+
token = os.environ.get("GITHUB_TOKEN")
73+
if not token:
74+
return (
75+
"Errore: GITHUB_TOKEN non impostato. "
76+
"Serve un token con scope 'workflow' per triggerare il build."
77+
)
78+
79+
response = requests.post(
80+
f"{_API_BASE}/actions/workflows/build-context.yml/dispatches",
81+
json={"ref": "main"},
82+
headers={
83+
"Authorization": f"token {token}",
84+
"Accept": "application/vnd.github+json",
85+
},
86+
timeout=10,
87+
)
88+
if response.status_code == 204:
89+
return "Build triggerato. Gli artifact saranno aggiornati entro ~1 minuto."
90+
return f"Errore: {response.status_code}{response.text}"
91+
92+
93+
def main() -> None:
94+
"""Entry point per l'MCP server."""
95+
mcp.run()
96+
97+
98+
if __name__ == "__main__":
99+
main()

tests/test_mcp_server.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""Tests for MCP server resources and tools."""
2+
3+
from unittest.mock import MagicMock, patch
4+
5+
6+
def _mock_response(text: str, status: int = 200):
7+
m = MagicMock()
8+
m.text = text
9+
m.status_code = status
10+
m.raise_for_status.return_value = None
11+
return m
12+
13+
14+
def test_session_bootstrap_resource():
15+
"""session_bootstrap fetches session_bootstrap.md from context branch."""
16+
from agent_context_builder.mcp_server import session_bootstrap
17+
18+
with patch("agent_context_builder.mcp_server.requests.get") as mock_get:
19+
mock_get.return_value = _mock_response("# Session Bootstrap\n")
20+
result = session_bootstrap()
21+
22+
assert "Session Bootstrap" in result
23+
called_url = mock_get.call_args[0][0]
24+
assert "session_bootstrap.md" in called_url
25+
assert "context" in called_url
26+
27+
28+
def test_workspace_triage_resource():
29+
"""workspace_triage fetches workspace_triage.json from context branch."""
30+
from agent_context_builder.mcp_server import workspace_triage
31+
32+
with patch("agent_context_builder.mcp_server.requests.get") as mock_get:
33+
mock_get.return_value = _mock_response('{"open_prs": 2}')
34+
result = workspace_triage()
35+
36+
assert "open_prs" in result
37+
assert "workspace_triage.json" in mock_get.call_args[0][0]
38+
39+
40+
def test_topic_index_resource():
41+
"""topic_index fetches topic_index.json from context branch."""
42+
from agent_context_builder.mcp_server import topic_index
43+
44+
with patch("agent_context_builder.mcp_server.requests.get") as mock_get:
45+
mock_get.return_value = _mock_response('{"topics": {}}')
46+
result = topic_index()
47+
48+
assert "topics" in result
49+
assert "topic_index.json" in mock_get.call_args[0][0]
50+
51+
52+
def test_refresh_context_no_token(monkeypatch):
53+
"""refresh_context returns error message when GITHUB_TOKEN not set."""
54+
from agent_context_builder.mcp_server import refresh_context
55+
56+
monkeypatch.delenv("GITHUB_TOKEN", raising=False)
57+
result = refresh_context()
58+
59+
assert "GITHUB_TOKEN" in result
60+
61+
62+
def test_refresh_context_success(monkeypatch):
63+
"""refresh_context triggers workflow dispatch and reports success."""
64+
from agent_context_builder.mcp_server import refresh_context
65+
66+
monkeypatch.setenv("GITHUB_TOKEN", "fake-token")
67+
with patch("agent_context_builder.mcp_server.requests.post") as mock_post:
68+
mock_post.return_value = _mock_response("", status=204)
69+
result = refresh_context()
70+
71+
assert "triggerato" in result.lower()
72+
73+
74+
def test_refresh_context_api_error(monkeypatch):
75+
"""refresh_context reports API errors without raising."""
76+
from agent_context_builder.mcp_server import refresh_context
77+
78+
monkeypatch.setenv("GITHUB_TOKEN", "fake-token")
79+
with patch("agent_context_builder.mcp_server.requests.post") as mock_post:
80+
mock_post.return_value = _mock_response("Forbidden", status=403)
81+
result = refresh_context()
82+
83+
assert "403" in result

0 commit comments

Comments
 (0)