Merge pull request 'Fix MCP registration and naming for Jenkins/GlitchTip servers (Issue #151)' (#226) from feat/issue-151-mcp-registration-naming into master

This commit was merged in pull request #226.
This commit is contained in:
2026-07-06 00:20:14 -05:00
8 changed files with 268 additions and 38 deletions
+8
View File
@@ -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
@@ -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).
@@ -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,
+5
View File
@@ -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,
+87
View File
@@ -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.
+46 -19
View File
@@ -3048,9 +3048,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.",
@@ -3216,28 +3216,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": {
@@ -3372,8 +3399,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:
@@ -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")
+19 -6
View File
@@ -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