From d747e0a2c958cd6710a697f7c7d46dcd01da151e Mon Sep 17 00:00:00 2001 From: Jason Walker <913443@dadeschools.net> Date: Mon, 6 Jul 2026 01:07:46 -0400 Subject: [PATCH] Document external MCP registration for Jenkins and GlitchTip --- README.md | 8 ++ .../glitchtip-readonly-tools-design.md | 20 +++-- .../jenkins-readonly-build-status-design.md | 14 ++- docs/llm-workflow-runbooks.md | 5 ++ docs/mcp-client-registration.md | 87 +++++++++++++++++++ gitea_mcp_server.py | 65 ++++++++++---- tests/test_external_mcp_registration_docs.py | 82 +++++++++++++++++ tests/test_operator_guide.py | 25 ++++-- 8 files changed, 268 insertions(+), 38 deletions(-) create mode 100644 docs/mcp-client-registration.md create mode 100644 tests/test_external_mcp_registration_docs.py diff --git a/README.md b/README.md index 1adac35..e099658 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,14 @@ Recognized environment fields (see [`.env.example`](.env.example) for placeholde | `GITEA_MCP_CONFIG` | Optional path to a JSON file defining multiple named runtime profiles. Unset ⇒ pure env behaviour. | | `GITEA_MCP_PROFILE` | Name of the profile (from `GITEA_MCP_CONFIG`) to activate for this runtime. | +#### External MCP Control Plane servers + +Jenkins and GlitchTip are separate MCP trust boundaries, not tools inside this +Gitea MCP runtime. Register them as `jenkins-mcp` and `glitchtip-mcp` in the +client that will use them, then reconnect or reload the client and verify the +expected tools are visible before claiming readiness. See +[`docs/mcp-client-registration.md`](docs/mcp-client-registration.md). + Notes: - This provides **one token + one profile per process**. It does not implement diff --git a/docs/architecture/glitchtip-readonly-tools-design.md b/docs/architecture/glitchtip-readonly-tools-design.md index 9de264d..804395c 100644 --- a/docs/architecture/glitchtip-readonly-tools-design.md +++ b/docs/architecture/glitchtip-readonly-tools-design.md @@ -22,13 +22,13 @@ Strictly read-only, per ADR-0001: tools — never one dual-credential server). - **This server never holds Gitea write credentials.** -## 2. Boundary placement (namespace pending) +## 2. Boundary placement These tools belong to the GlitchTip observability boundary of the MCP Control -Plane — `glitchtip-mcp` (ADR-0001's recommendation), `observability-mcp`, or -folded into `ops-mcp`. **ADR-0001 open owner decision #2 picks the name; this -design does not assume it.** Tool names below use the `glitchtip_` prefix for -readability and rename mechanically with the decision. +Plane. The canonical MCP server name is `glitchtip-mcp`; client registration +and reload instructions live in +[`../mcp-client-registration.md`](../mcp-client-registration.md). Tool names +below use the `glitchtip_` prefix. Fixed regardless of the name (per `tool-boundaries.md`, `credential-isolation.md`): @@ -160,11 +160,13 @@ Mocked-GlitchTip unit tests only, per `docs/developer-testing-guidelines.md`: ## 10. Implementation-readiness checklist -Ready to implement once: +Ready to operate once: -1. ADR-0001 owner decision #2 (namespace/placement) is made — mechanical - rename of the `glitchtip_` prefix if needed. -2. ADR-0001 owner decision #1 (repo home) is made. +1. The MCP client registers the external server under the exact `glitchtip-mcp` + name and is reconnected/reloaded. +2. Tool discoverability proves the expected `glitchtip_*` read tools are + visible; if the server is enabled but exposes no usable tools, report + `SKIPPED` and stop. 3. #76 profile schema exists (or a minimal `glitchtip-readonly` profile is hand-rolled to the same rules). 4. A pinned GlitchTip version is chosen for API-subset testing (§3). diff --git a/docs/architecture/jenkins-readonly-build-status-design.md b/docs/architecture/jenkins-readonly-build-status-design.md index 27dbaaf..bc5eff1 100644 --- a/docs/architecture/jenkins-readonly-build-status-design.md +++ b/docs/architecture/jenkins-readonly-build-status-design.md @@ -6,6 +6,8 @@ - **Date:** 2026-07-02 Note on naming: This design used historical `jenkins-readonly` skill name in Gitea-Tools. Actual package/server is `jenkins-mcp` (see mcp-control-plane registration in #55). The server boundary now contains gated trigger (see #56), but read tools remain as designed. +Client registration and reload instructions live in +[`../mcp-client-registration.md`](../mcp-client-registration.md). ## 1. Purpose and scope @@ -141,12 +143,16 @@ repo's conventions (`docs/developer-testing-guidelines.md`): ## 10. Implementation-readiness checklist -Ready to implement in `jenkins-mcp` once: +Ready to operate through `jenkins-mcp` once: -1. ADR-0001 owner decision #1 (where `jenkins-mcp` lives) is made. -2. #76 profile schema exists (or a minimal `jenkins-readonly` profile is +1. The MCP client registers the external server under the exact `jenkins-mcp` + name and is reconnected/reloaded. +2. Tool discoverability proves the expected `jenkins_*` read tools are visible; + if the server is enabled but exposes no usable tools, report `SKIPPED` and + stop. +3. #76 profile schema exists (or a minimal `jenkins-readonly` profile is hand-rolled to the same rules). -3. #77 mapping design is accepted (or tools ship path-addressed only, mapping +4. #77 mapping design is accepted (or tools ship path-addressed only, mapping deferred). Explicitly **not** unlocked by this document: build triggers, deploys, diff --git a/docs/llm-workflow-runbooks.md b/docs/llm-workflow-runbooks.md index 19ca116..3bcc708 100644 --- a/docs/llm-workflow-runbooks.md +++ b/docs/llm-workflow-runbooks.md @@ -30,6 +30,11 @@ audit logging). See [Related documents](#related-documents). > the standard rules; operator prompts still control task-specific scope. > See issue #129 for the skill registry design. +Jenkins and GlitchTip workflows use separate MCP servers, not this Gitea MCP +runtime. Register them as `jenkins-mcp` and `glitchtip-mcp`, reconnect or +reload the client, and verify visible tools before claiming either integration +is usable. See [`mcp-client-registration.md`](mcp-client-registration.md). + For cross-project use, copy the portable workflow skill at [`../skills/llm-project-workflow/SKILL.md`](../skills/llm-project-workflow/SKILL.md). It extracts the issue-first, isolated-worktree, no-self-review, profile-safety, diff --git a/docs/mcp-client-registration.md b/docs/mcp-client-registration.md new file mode 100644 index 0000000..049e57b --- /dev/null +++ b/docs/mcp-client-registration.md @@ -0,0 +1,87 @@ +# MCP Client Registration for External Control Plane Servers + +Issue #151 fixes the registration and naming contract for the Jenkins and +GlitchTip MCP servers that live outside this Gitea MCP runtime. + +## Canonical Server Names + +Use these exact MCP server names in clients: + +| Server name | Boundary | Default capability | +|---|---|---| +| `jenkins-mcp` | Jenkins CI inspection | Read-only build/job inspection | +| `glitchtip-mcp` | GlitchTip observability inspection | Read-only issue/event inspection | + +Historical names such as `jenkins-readonly` and `glitchtip-readonly` are +descriptive profile labels only. They are not the canonical MCP server names +unless an operator intentionally creates aliases and documents them. + +## Registration Pattern + +Register each external server as its own MCP entry. Do not add Jenkins or +GlitchTip credentials to the Gitea MCP server. Also, do not add Gitea write +credentials to the GlitchTip server. + +```json +{ + "mcpServers": { + "jenkins-mcp": { + "command": "/path/to/mcp-control-plane/venv/bin/python3", + "args": ["-m", "jenkins_mcp"], + "env": { + "JENKINS_MCP_CONFIG": "/path/to/mcp-control-plane/profiles.json", + "JENKINS_MCP_PROFILE": "jenkins-readonly" + } + }, + "glitchtip-mcp": { + "command": "/path/to/mcp-control-plane/venv/bin/python3", + "args": ["-m", "glitchtip_mcp"], + "env": { + "GLITCHTIP_MCP_CONFIG": "/path/to/mcp-control-plane/profiles.json", + "GLITCHTIP_MCP_PROFILE": "glitchtip-readonly" + } + } + } +} +``` + +Client-specific wrappers may differ, but the server names, trust boundaries, +and profile separation must remain the same. After adding or changing either +entry, reconnect or reload the MCP client before claiming the tools are usable. + +## Discoverability Validation + +Before using either server in a task, prove the expected tools are visible in +the client. It is not enough for the config entry to exist. + +Expected Jenkins tools: + +- `jenkins_whoami` +- `jenkins_list_jobs` +- `jenkins_latest_build` +- `jenkins_build_status` +- `jenkins_get_build` + +Expected GlitchTip tools: + +- `glitchtip_whoami` +- `glitchtip_list_projects` +- `glitchtip_list_unresolved` +- `glitchtip_get_issue` +- `glitchtip_recent_events` +- `glitchtip_search` + +If a client reports the server as enabled but exposes no usable tools, report +`SKIPPED: server enabled but no usable tools visible`, then stop. Do not fall +back to shell commands, raw service APIs, or unrelated MCP servers. + +## Boundary Rules + +- `jenkins-mcp` read profiles must not expose build trigger tools. +- Jenkins build triggers require a separately named write profile, exact + confirmation, and fail-closed mutation audit. +- `glitchtip-mcp` remains read-only. It must not file or mutate Gitea issues. +- GlitchTip-to-Gitea filing is a separate orchestrator that composes GlitchTip + read tools with Gitea issue-write tools. +- Gitea credentials never enter Jenkins or GlitchTip runtimes. +- Jenkins and GlitchTip credentials never enter the Gitea MCP runtime. diff --git a/gitea_mcp_server.py b/gitea_mcp_server.py index 19d168d..a3d5243 100644 --- a/gitea_mcp_server.py +++ b/gitea_mcp_server.py @@ -2947,9 +2947,9 @@ _COMMON_WORKFLOWS = [ ] # Skill registry (#128). status: 'available' = backed by tools in this -# server; 'designed-not-implemented' = design exists but no MCP tools yet -# (listed rather than omitted so sessions know the boundary); 'operator-only' -# = never performed by an LLM session. +# server; 'external-mcp' = implemented in another MCP server that must be +# registered separately; 'designed-not-implemented' = design exists but no MCP +# tools yet; 'operator-only' = never performed by an LLM session. _PROJECT_SKILLS = { "gitea-issue-authoring": { "description": "Create, view, label, claim, and close Gitea issues.", @@ -3115,28 +3115,55 @@ _PROJECT_SKILLS = { "jenkins-mcp": { "description": "Read-only Jenkins CI inspection (jobs, builds, " "logs).", - "when_to_use": "Checking CI state once Jenkins MCP tools exist.", + "when_to_use": "Checking CI state through a separately registered " + "jenkins-mcp server.", "required_operations": ["jenkins.read"], - "status": "designed-not-implemented", - "notes": "Server code exists in mcp-control-plane as jenkins-mcp (read tools + gated trigger); registration pending. To register for discoverability in clients (Codex/Gemini/Grok/etc.): add to client MCP config under the jenkins-mcp name, reconnect/reload the client session after registration. Report SKIPPED if not connected. Do not substitute shell/API. Trigger requires dedicated profile (see #56).", + "status": "external-mcp", + "notes": "Server code lives in mcp-control-plane as the separate " + "jenkins-mcp server. Register that server under the exact " + "jenkins-mcp name in the MCP client, reconnect/reload the " + "client, then verify Jenkins tools are visible before use. " + "Report SKIPPED if the server is absent or exposes no usable " + "tools. Do not substitute shell/API. Trigger requires a " + "dedicated write profile and confirmation (see #56).", "steps": [ - "Confirm a Jenkins MCP server is connected (jenkins-mcp); if not, report " - "SKIPPED.", - "Use read-only operations only; never trigger unless using dedicated profile + confirmation.", + "Confirm a Jenkins MCP server is connected under the exact " + "jenkins-mcp name.", + "Verify tool discoverability: jenkins_whoami, " + "jenkins_list_jobs, jenkins_latest_build, jenkins_build_status, " + "and jenkins_get_build must be visible before claiming Jenkins " + "readiness.", + "If the server is enabled but exposes no usable tools, report " + "SKIPPED and stop.", + "Use read-only operations only; never trigger unless using a " + "dedicated write profile plus exact confirmation.", ], }, "glitchtip-mcp": { "description": "Read-only GlitchTip error/event inspection.", - "when_to_use": "Investigating reported errors once GlitchTip MCP " - "tools exist.", + "when_to_use": "Investigating reported errors through a separately " + "registered glitchtip-mcp server.", "required_operations": ["glitchtip.read"], - "status": "designed-not-implemented", - "notes": "Server code exists in mcp-control-plane as glitchtip-mcp (read-only tools); registration pending. To register for discoverability in clients (Codex/Gemini/Grok/etc.): add to client MCP config under the glitchtip-mcp name, reconnect/reload the client session after registration. Filing orchestrator is partial in mcp-control-plane (see #57). Report SKIPPED if not connected. Filing to Gitea is separate orchestrator, not in this server.", + "status": "external-mcp", + "notes": "Server code lives in mcp-control-plane as the separate " + "glitchtip-mcp server. Register that server under the exact " + "glitchtip-mcp name in the MCP client, reconnect/reload the " + "client, then verify GlitchTip tools are visible before use. " + "Report SKIPPED if the server is absent or exposes no usable " + "tools. Filing to Gitea is a separate orchestrator tracked " + "outside this server (see #57).", "steps": [ - "Confirm a GlitchTip MCP server is connected (glitchtip-mcp); if not, report " - "SKIPPED.", - "Use read-only operations only; never mutate issues or " - "settings. Filing tracked in #57.", + "Confirm a GlitchTip MCP server is connected under the exact " + "glitchtip-mcp name.", + "Verify tool discoverability: glitchtip_whoami, " + "glitchtip_list_projects, glitchtip_list_unresolved, " + "glitchtip_get_issue, glitchtip_recent_events, and " + "glitchtip_search must be visible before claiming GlitchTip " + "readiness.", + "If the server is enabled but exposes no usable tools, report " + "SKIPPED and stop.", + "Use read-only operations only; never mutate issues or settings. " + "Filing is a separate orchestrator tracked in #57.", ], }, "release-operator": { @@ -3271,8 +3298,8 @@ def mcp_list_project_skills() -> dict: Read-only; makes no API calls. Each skill reports its name, description, when-to-use guidance, required operations, status - ('available', 'designed-not-implemented', or 'operator-only'), and - whether the current profile's operations permit it. Use + ('available', 'external-mcp', 'designed-not-implemented', or + 'operator-only'), and whether the current profile's operations permit it. Use mcp_get_skill_guide(name) for the step-by-step guide. Returns: diff --git a/tests/test_external_mcp_registration_docs.py b/tests/test_external_mcp_registration_docs.py new file mode 100644 index 0000000..c14bc52 --- /dev/null +++ b/tests/test_external_mcp_registration_docs.py @@ -0,0 +1,82 @@ +"""Documentation checks for external Jenkins/GlitchTip MCP registration (#151).""" +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +DOC = REPO_ROOT / "docs" / "mcp-client-registration.md" + + +def _doc_text(): + assert DOC.is_file(), "missing docs/mcp-client-registration.md" + return DOC.read_text(encoding="utf-8") + + +def test_registration_doc_names_canonical_servers(): + text = _doc_text() + assert "jenkins-mcp" in text + assert "glitchtip-mcp" in text + assert "Historical names" in text + assert "jenkins-readonly" in text + assert "glitchtip-readonly" in text + + +def test_registration_doc_lists_expected_tool_visibility(): + text = _doc_text() + for tool in ( + "jenkins_whoami", + "jenkins_list_jobs", + "jenkins_latest_build", + "jenkins_build_status", + "jenkins_get_build", + "glitchtip_whoami", + "glitchtip_list_projects", + "glitchtip_list_unresolved", + "glitchtip_get_issue", + "glitchtip_recent_events", + "glitchtip_search", + ): + assert tool in text + + +def test_registration_doc_requires_reload_and_negative_case(): + text = _doc_text() + lower = text.lower() + assert "reconnect" in lower + assert "reload" in lower + assert "enabled but no usable tools" in lower + assert "SKIPPED" in text + + +def test_registration_doc_preserves_boundaries(): + text = " ".join(_doc_text().split()) + for phrase in ( + "Do not add Jenkins or GlitchTip credentials to the Gitea MCP server", + "do not add Gitea write credentials to the GlitchTip server", + "must not expose build trigger tools", + "remains read-only", + ): + assert phrase in text + + +def test_registration_doc_has_no_secret_material_or_live_urls(): + text = _doc_text() + for marker in ( + "https://", + "http://", + "Authorization:", + "BEGIN PRIVATE KEY", + "ghp_", + "token-value", + ): + assert marker not in text + + +def test_related_docs_link_registration_doc(): + for name in ( + "README.md", + "docs/llm-workflow-runbooks.md", + "docs/architecture/jenkins-readonly-build-status-design.md", + "docs/architecture/glitchtip-readonly-tools-design.md", + ): + text = (REPO_ROOT / name).read_text(encoding="utf-8") + assert "mcp-client-registration.md" in text, ( + f"{name} does not link docs/mcp-client-registration.md") diff --git a/tests/test_operator_guide.py b/tests/test_operator_guide.py index 36767aa..80c07f4 100644 --- a/tests/test_operator_guide.py +++ b/tests/test_operator_guide.py @@ -234,8 +234,8 @@ class TestProjectSkills(GuideTestBase): with patch.dict(os.environ, AUTHOR_ENV, clear=True): r = mcp_list_project_skills() by_name = {s["name"]: s for s in r["skills"]} - self.assertNotEqual(by_name["jenkins-mcp"]["status"], "available") - self.assertNotEqual(by_name["glitchtip-mcp"]["status"], "available") + self.assertEqual(by_name["jenkins-mcp"]["status"], "external-mcp") + self.assertEqual(by_name["glitchtip-mcp"]["status"], "external-mcp") def test_no_urls_in_registry(self): with patch.dict(os.environ, AUTHOR_ENV, clear=True): @@ -246,16 +246,29 @@ class TestProjectSkills(GuideTestBase): self.assertNotIn("keychain:", blob) def test_enabled_but_no_usable_tools_negative_assertion(self): - """Negative assertion for 'enabled but no usable tools' (per issue #146).""" + """Negative assertion for 'enabled but no usable tools' (per issue #151).""" with patch.dict(os.environ, AUTHOR_ENV, clear=True): r = mcp_list_project_skills() by_name = {s["name"]: s for s in r["skills"]} - # jenkins-mcp is designed-not-implemented; even if "enabled" in config, - # it should not be usable/available to current profile without tools. + # jenkins-mcp is an external boundary; Gitea profile discovery must not + # treat a local config entry as a usable Jenkins tool surface. self.assertIn("jenkins-mcp", by_name) - self.assertEqual(by_name["jenkins-mcp"]["status"], "designed-not-implemented") + self.assertEqual(by_name["jenkins-mcp"]["status"], "external-mcp") self.assertFalse(by_name["jenkins-mcp"].get("available_to_current_profile", False)) + def test_external_mcp_skill_guides_name_expected_tools(self): + with patch.dict(os.environ, AUTHOR_ENV, clear=True): + jenkins = mcp_get_skill_guide("jenkins-mcp") + glitchtip = mcp_get_skill_guide("glitchtip-mcp") + self.assertEqual(jenkins["skill"]["status"], "external-mcp") + self.assertIn("jenkins_whoami", " ".join(jenkins["steps"])) + self.assertIn("jenkins_get_build", " ".join(jenkins["steps"])) + self.assertIn("SKIPPED", " ".join(jenkins["steps"])) + self.assertEqual(glitchtip["skill"]["status"], "external-mcp") + self.assertIn("glitchtip_whoami", " ".join(glitchtip["steps"])) + self.assertIn("glitchtip_search", " ".join(glitchtip["steps"])) + self.assertIn("SKIPPED", " ".join(glitchtip["steps"])) + # --------------------------------------------------------------------------- # mcp_get_skill_guide