feat: support separate Gitea MCP runtime profiles via env config (#19)
Allow the same MCP server to run as separate MCP entries, each with its own token and profile name, so roles stay task-scoped (the profile is the role, not the LLM). - gitea_auth.get_profile(): reads GITEA_PROFILE_NAME, GITEA_ALLOWED_OPERATIONS, GITEA_BASE_URL as non-secret metadata. Never reads/returns/logs the token. - gitea_whoami now surfaces the safe profile metadata (name + allowed operations) alongside identity; token still never exposed. - .env.example: placeholder-only template for a runtime profile. - .gitignore: track .env.example while keeping real .env* ignored. - README: document multiple env-configured MCP entries. - tests: profile defaults/parsing, token-never-included, whoami surfaces profile without leaking token. One token + one profile per process. No multi-token switching in a single runtime. No approve/merge/eligibility workflow. No Jenkins/Ops/GlitchTip/Release/deploy behavior. No real secrets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ from mcp_server import ( # noqa: E402
|
||||
gitea_commit_files,
|
||||
gitea_whoami,
|
||||
)
|
||||
from gitea_auth import get_profile # noqa: E402
|
||||
|
||||
FAKE_AUTH = "Basic dGVzdDp0ZXN0"
|
||||
|
||||
@@ -514,5 +515,60 @@ class TestWhoami(unittest.TestCase):
|
||||
gitea_whoami(remote="nope")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Runtime profile (env-configured profile metadata) — issue #19
|
||||
# ---------------------------------------------------------------------------
|
||||
class TestRuntimeProfile(unittest.TestCase):
|
||||
|
||||
def test_defaults_when_unset(self):
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
p = get_profile()
|
||||
self.assertEqual(p["profile_name"], "gitea-default")
|
||||
self.assertEqual(p["allowed_operations"], [])
|
||||
self.assertIsNone(p["base_url"])
|
||||
|
||||
def test_reads_env_metadata(self):
|
||||
env = {
|
||||
"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read, review , approve",
|
||||
"GITEA_BASE_URL": "https://gitea.example.invalid",
|
||||
}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
p = get_profile()
|
||||
self.assertEqual(p["profile_name"], "gitea-reviewer")
|
||||
self.assertEqual(p["allowed_operations"], ["read", "review", "approve"])
|
||||
self.assertEqual(p["base_url"], "https://gitea.example.invalid")
|
||||
|
||||
def test_never_includes_token(self):
|
||||
env = {
|
||||
"GITEA_PROFILE_NAME": "gitea-author",
|
||||
"GITEA_TOKEN": "super-secret-token",
|
||||
}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
p = get_profile()
|
||||
blob = repr(p).lower()
|
||||
self.assertNotIn("super-secret-token", blob)
|
||||
self.assertNotIn("token", blob)
|
||||
|
||||
@patch("mcp_server.api_request")
|
||||
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
|
||||
def test_whoami_surfaces_profile_without_token(self, _auth, mock_api):
|
||||
mock_api.return_value = {"id": 7, "login": "rev"}
|
||||
env = {
|
||||
"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review,approve",
|
||||
"GITEA_TOKEN": "super-secret-token",
|
||||
}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
result = gitea_whoami(remote="prgs")
|
||||
self.assertEqual(result["profile"]["profile_name"], "gitea-reviewer")
|
||||
self.assertEqual(
|
||||
result["profile"]["allowed_operations"], ["read", "review", "approve"]
|
||||
)
|
||||
blob = repr(result).lower()
|
||||
for secret in ("super-secret-token", "token", "authorization", "basic "):
|
||||
self.assertNotIn(secret, blob)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user