gitlab-api 25.43.0__tar.gz → 25.46.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/PKG-INFO +39 -6
  2. gitlab_api-25.43.0/gitlab_api.egg-info/PKG-INFO → gitlab_api-25.46.0/README.md +34 -33
  3. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/__init__.py +1 -1
  4. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/agent_server.py +1 -1
  5. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/auth.py +57 -9
  6. gitlab_api-25.46.0/gitlab_api/instances.py +110 -0
  7. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_branches.py +12 -4
  8. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_commits.py +34 -12
  9. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_custom_api.py +4 -1
  10. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_deploy_tokens.py +27 -9
  11. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_environments.py +35 -13
  12. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_epics.py +13 -5
  13. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_graphql.py +10 -4
  14. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_groups.py +24 -7
  15. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_issues.py +13 -5
  16. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_jobs.py +25 -7
  17. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_labels.py +13 -5
  18. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_members.py +10 -2
  19. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_merge_requests.py +12 -4
  20. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_merge_rules.py +41 -12
  21. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_milestones.py +13 -5
  22. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_notes.py +13 -5
  23. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_packages.py +11 -3
  24. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_pipeline_schedules.py +37 -10
  25. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_pipelines.py +9 -3
  26. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_projects.py +26 -8
  27. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_protected_branches.py +12 -4
  28. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_releases.py +34 -12
  29. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_runners.py +41 -15
  30. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_snippets.py +13 -5
  31. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_tags.py +26 -8
  32. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp_server.py +521 -171
  33. gitlab_api-25.43.0/README.md → gitlab_api-25.46.0/gitlab_api.egg-info/PKG-INFO +66 -1
  34. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api.egg-info/SOURCES.txt +3 -0
  35. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api.egg-info/requires.txt +4 -4
  36. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/pyproject.toml +5 -5
  37. gitlab_api-25.46.0/requirements.txt +1 -0
  38. gitlab_api-25.46.0/tests/test_action_discovery.py +70 -0
  39. gitlab_api-25.46.0/tests/test_instances.py +101 -0
  40. gitlab_api-25.43.0/requirements.txt +0 -1
  41. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/LICENSE +0 -0
  42. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/MANIFEST.in +0 -0
  43. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/__main__.py +0 -0
  44. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/api/__init__.py +0 -0
  45. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/api/api_client_base.py +0 -0
  46. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/api/api_client_environments.py +0 -0
  47. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/api/api_client_issues.py +0 -0
  48. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/api/api_client_merge_requests.py +0 -0
  49. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/api/api_client_other.py +0 -0
  50. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/api/api_client_pipelines.py +0 -0
  51. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/api/api_client_projects.py +0 -0
  52. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/api/api_client_repositories.py +0 -0
  53. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/api/api_client_system.py +0 -0
  54. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/api/api_client_users_groups.py +0 -0
  55. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/api_client.py +0 -0
  56. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/gitlab_gql.py +0 -0
  57. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/gitlab_input_models.py +0 -0
  58. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/gitlab_response_models.py +0 -0
  59. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/main_agent.json +0 -0
  60. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/__init__.py +0 -0
  61. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp/mcp_misc.py +0 -0
  62. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api/mcp_config.json +0 -0
  63. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api.egg-info/dependency_links.txt +0 -0
  64. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api.egg-info/entry_points.txt +0 -0
  65. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/gitlab_api.egg-info/top_level.txt +0 -0
  66. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/scripts/security_sanitizer.py +0 -0
  67. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/scripts/validate_a2a_agent.py +0 -0
  68. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/scripts/validate_agent.py +0 -0
  69. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/scripts/verify_api_integration.py +0 -0
  70. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/setup.cfg +0 -0
  71. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/tests/__init__.py +0 -0
  72. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/tests/conftest.py +0 -0
  73. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/tests/test_api_wrapper.py +0 -0
  74. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/tests/test_auth.py +0 -0
  75. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/tests/test_concept_parity.py +0 -0
  76. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/tests/test_gitlab_a2a_validation.py +0 -0
  77. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/tests/test_gitlab_api_brute_force_coverage.py +0 -0
  78. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/tests/test_gitlab_gql.py +0 -0
  79. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/tests/test_gitlab_mcp_validation.py +0 -0
  80. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/tests/test_gitlab_models.py +0 -0
  81. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/tests/test_init_dynamics.py +0 -0
  82. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/tests/test_mock_coverage.py +0 -0
  83. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/tests/test_startup.py +0 -0
  84. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/tests/test_verify_agent.py +0 -0
  85. {gitlab_api-25.43.0 → gitlab_api-25.46.0}/tests/verify_a2a_queries.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitlab-api
3
- Version: 25.43.0
3
+ Version: 25.46.0
4
4
  Summary: GitLab API + MCP Server + A2A Server
5
5
  Author-email: Audel Rouhi <knucklessg1@gmail.com>
6
6
  License: MIT
@@ -12,17 +12,17 @@ Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: <3.15,>=3.11
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
- Requires-Dist: agent-utilities>=0.48.0
15
+ Requires-Dist: agent-utilities>=0.51.0
16
16
  Requires-Dist: python-dotenv>=1.0.0
17
17
  Requires-Dist: gql[requests]>=4.0.0
18
18
  Provides-Extra: mcp
19
- Requires-Dist: agent-utilities[mcp]>=0.48.0; extra == "mcp"
19
+ Requires-Dist: agent-utilities[mcp]>=0.51.0; extra == "mcp"
20
20
  Provides-Extra: agent
21
- Requires-Dist: agent-utilities[agent,logfire]>=0.48.0; extra == "agent"
21
+ Requires-Dist: agent-utilities[agent,logfire]>=0.51.0; extra == "agent"
22
22
  Provides-Extra: gql
23
23
  Requires-Dist: gql[requests]>=4.0.0; extra == "gql"
24
24
  Provides-Extra: all
25
- Requires-Dist: gitlab-api[agent,gql,logfire,mcp]>=25.43.0; extra == "all"
25
+ Requires-Dist: gitlab-api[agent,gql,logfire,mcp]>=25.46.0; extra == "all"
26
26
  Provides-Extra: test
27
27
  Requires-Dist: pytest-xdist>=3.6.0; extra == "test"
28
28
  Requires-Dist: pytest; extra == "test"
@@ -52,7 +52,7 @@ Dynamic: license-file
52
52
  ![PyPI - Wheel](https://img.shields.io/pypi/wheel/gitlab-api)
53
53
  ![PyPI - Implementation](https://img.shields.io/pypi/implementation/gitlab-api)
54
54
 
55
- *Version: 25.43.0*
55
+ *Version: 25.46.0*
56
56
 
57
57
  > **Documentation** — Installation, deployment, usage across the API, CLI, and MCP
58
58
  > interfaces, the integrated A2A agent server, and guidance for provisioning the
@@ -320,6 +320,39 @@ Detailed graph node architecture explanations, custom skill configurations, and
320
320
 
321
321
  ---
322
322
 
323
+ ## Multi-Tenancy (multiple GitLab instances)
324
+
325
+ The client is natively multi-tenant. The set of instances is declared once in the
326
+ shared **agent-utilities XDG config** (`~/.config/agent-utilities/config.json`) under
327
+ `gitlab_instances` — the **same** list the Knowledge-Graph GitLab indexer reads, so one
328
+ config drives both code/metadata indexing and every API/MCP call:
329
+
330
+ ```json
331
+ {
332
+ "gitlab_instances": [
333
+ {"name": "internal", "url": "https://gitlab.arpa", "token": "glpat-xxxx", "verify_ssl": false},
334
+ {"name": "public", "url": "https://gitlab.com", "token": "glpat-yyyy", "verify_ssl": true}
335
+ ]
336
+ }
337
+ ```
338
+
339
+ Target a tenant by **name** from the client factory; a bare URL still works, and an
340
+ unset instance resolves to the first configured one (else `GITLAB_URL`/`GITLAB_TOKEN`):
341
+
342
+ ```python
343
+ from gitlab_api.auth import get_client
344
+ from gitlab_api.instances import list_configured_instances
345
+
346
+ internal = get_client(instance="internal") # resolves url+token+verify from config
347
+ public = get_client(instance="public")
348
+ default = get_client() # first configured / GITLAB_URL fallback
349
+ names = [i.name for i in list_configured_instances()]
350
+ ```
351
+
352
+ The MCP server exposes a `gitlab_instances` tool (`action=list|get`) to discover the
353
+ configured tenants (tokens are never returned). When no instances are configured, the
354
+ connector falls back to the single-host `GITLAB_URL`/`GITLAB_TOKEN` it has always used.
355
+
323
356
  ## Security & Governance
324
357
 
325
358
  Built directly upon the enterprise-ready [`agent-utilities`](https://github.com/Knuckles-Team/agent-utilities) core, standard security parameters are fully supported:
@@ -1,35 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: gitlab-api
3
- Version: 25.43.0
4
- Summary: GitLab API + MCP Server + A2A Server
5
- Author-email: Audel Rouhi <knucklessg1@gmail.com>
6
- License: MIT
7
- Classifier: Development Status :: 5 - Production/Stable
8
- Classifier: License :: OSI Approved :: MIT License
9
- Classifier: Environment :: Console
10
- Classifier: Operating System :: POSIX :: Linux
11
- Classifier: Programming Language :: Python :: 3
12
- Requires-Python: <3.15,>=3.11
13
- Description-Content-Type: text/markdown
14
- License-File: LICENSE
15
- Requires-Dist: agent-utilities>=0.48.0
16
- Requires-Dist: python-dotenv>=1.0.0
17
- Requires-Dist: gql[requests]>=4.0.0
18
- Provides-Extra: mcp
19
- Requires-Dist: agent-utilities[mcp]>=0.48.0; extra == "mcp"
20
- Provides-Extra: agent
21
- Requires-Dist: agent-utilities[agent,logfire]>=0.48.0; extra == "agent"
22
- Provides-Extra: gql
23
- Requires-Dist: gql[requests]>=4.0.0; extra == "gql"
24
- Provides-Extra: all
25
- Requires-Dist: gitlab-api[agent,gql,logfire,mcp]>=25.43.0; extra == "all"
26
- Provides-Extra: test
27
- Requires-Dist: pytest-xdist>=3.6.0; extra == "test"
28
- Requires-Dist: pytest; extra == "test"
29
- Requires-Dist: pytest-asyncio; extra == "test"
30
- Requires-Dist: pytest-cov; extra == "test"
31
- Dynamic: license-file
32
-
33
1
  # Gitlab Api
34
2
  ## CLI or API | MCP | Agent
35
3
 
@@ -52,7 +20,7 @@ Dynamic: license-file
52
20
  ![PyPI - Wheel](https://img.shields.io/pypi/wheel/gitlab-api)
53
21
  ![PyPI - Implementation](https://img.shields.io/pypi/implementation/gitlab-api)
54
22
 
55
- *Version: 25.43.0*
23
+ *Version: 25.46.0*
56
24
 
57
25
  > **Documentation** — Installation, deployment, usage across the API, CLI, and MCP
58
26
  > interfaces, the integrated A2A agent server, and guidance for provisioning the
@@ -320,6 +288,39 @@ Detailed graph node architecture explanations, custom skill configurations, and
320
288
 
321
289
  ---
322
290
 
291
+ ## Multi-Tenancy (multiple GitLab instances)
292
+
293
+ The client is natively multi-tenant. The set of instances is declared once in the
294
+ shared **agent-utilities XDG config** (`~/.config/agent-utilities/config.json`) under
295
+ `gitlab_instances` — the **same** list the Knowledge-Graph GitLab indexer reads, so one
296
+ config drives both code/metadata indexing and every API/MCP call:
297
+
298
+ ```json
299
+ {
300
+ "gitlab_instances": [
301
+ {"name": "internal", "url": "https://gitlab.arpa", "token": "glpat-xxxx", "verify_ssl": false},
302
+ {"name": "public", "url": "https://gitlab.com", "token": "glpat-yyyy", "verify_ssl": true}
303
+ ]
304
+ }
305
+ ```
306
+
307
+ Target a tenant by **name** from the client factory; a bare URL still works, and an
308
+ unset instance resolves to the first configured one (else `GITLAB_URL`/`GITLAB_TOKEN`):
309
+
310
+ ```python
311
+ from gitlab_api.auth import get_client
312
+ from gitlab_api.instances import list_configured_instances
313
+
314
+ internal = get_client(instance="internal") # resolves url+token+verify from config
315
+ public = get_client(instance="public")
316
+ default = get_client() # first configured / GITLAB_URL fallback
317
+ names = [i.name for i in list_configured_instances()]
318
+ ```
319
+
320
+ The MCP server exposes a `gitlab_instances` tool (`action=list|get`) to discover the
321
+ configured tenants (tokens are never returned). When no instances are configured, the
322
+ connector falls back to the single-host `GITLAB_URL`/`GITLAB_TOKEN` it has always used.
323
+
323
324
  ## Security & Governance
324
325
 
325
326
  Built directly upon the enterprise-ready [`agent-utilities`](https://github.com/Knuckles-Team/agent-utilities) core, standard security parameters are fully supported:
@@ -37,7 +37,7 @@ for module_name in CORE_MODULES:
37
37
  _expose_members(module)
38
38
 
39
39
  # Dynamic/lazy loading of optional modules (agent_server, mcp_server)
40
- _loaded_optional_modules = {}
40
+ _loaded_optional_modules: dict[str, Any] = {}
41
41
 
42
42
 
43
43
  def _import_module_safely(module_name: str):
@@ -4,7 +4,7 @@ import os
4
4
  import sys
5
5
  import warnings
6
6
 
7
- __version__ = "25.43.0"
7
+ __version__ = "25.46.0"
8
8
 
9
9
  logging.basicConfig(
10
10
  level=logging.INFO,
@@ -22,17 +22,62 @@ from gitlab_api.api_client import Api
22
22
  logger = get_logger(__name__)
23
23
 
24
24
 
25
+ def _resolve_connection(
26
+ instance: str | None, token: str | None, verify: bool | None
27
+ ) -> tuple[str, str | None, bool]:
28
+ """Resolve ``(url, token, verify)`` for a target tenant (CONCEPT:KG-2.9g).
29
+
30
+ ``instance`` may be a configured instance NAME (resolved from the shared
31
+ ``gitlab_instances`` config), a bare URL (used as-is, back-compat), or ``None``
32
+ (the default instance, else ``GITLAB_URL``). Explicit ``token``/``verify`` args
33
+ always win over the instance's stored values.
34
+ """
35
+ from gitlab_api.instances import get_instance
36
+
37
+ env_verify = to_boolean(string=os.getenv("GITLAB_SSL_VERIFY", "True"))
38
+
39
+ # A bare URL is used directly (the historical call shape) — the caller owns
40
+ # the token it passes (no env fallback, so an explicit token=None stays None).
41
+ if instance and str(instance).startswith(("http://", "https://")):
42
+ return (instance, token, env_verify if verify is None else verify)
43
+
44
+ # A name (or None=default) resolves against the configured tenants.
45
+ inst = get_instance(instance)
46
+ if inst is None:
47
+ if instance:
48
+ raise RuntimeError(
49
+ f"GitLab instance '{instance}' is not configured. Add it to "
50
+ "gitlab_instances in ~/.config/agent-utilities/config.json, or pass "
51
+ "a full URL / set GITLAB_URL+GITLAB_TOKEN."
52
+ )
53
+ # No config at all → legacy single-host env defaults.
54
+ return (
55
+ os.getenv("GITLAB_URL", "https://gitlab.com"),
56
+ token or os.getenv("GITLAB_TOKEN"),
57
+ env_verify if verify is None else verify,
58
+ )
59
+ return (
60
+ inst.url,
61
+ token or inst.token or os.getenv("GITLAB_TOKEN"),
62
+ inst.verify_ssl if verify is None else verify,
63
+ )
64
+
65
+
25
66
  def get_client(
26
- instance: str = os.getenv("GITLAB_URL", "https://gitlab.com"),
27
- token: str | None = os.getenv("GITLAB_TOKEN", None),
28
- verify: bool = to_boolean(string=os.getenv("GITLAB_SSL_VERIFY", "True")),
67
+ instance: str | None = None,
68
+ token: str | None = None,
69
+ verify: bool | None = None,
29
70
  config: dict | None = None,
30
71
  ) -> Api:
31
72
  """Factory function to create the GitLab Api client.
32
73
 
33
- Supports OIDC delegation and fixed credentials (token).
34
- Uses the shared ``delegated_auth`` helper from agent-utilities.
74
+ Multi-tenant (CONCEPT:KG-2.9g): ``instance`` selects a configured tenant by
75
+ name (from the shared ``gitlab_instances`` config), accepts a bare URL, or
76
+ defaults to the first configured instance / ``GITLAB_URL``. Supports OIDC
77
+ delegation and fixed credentials (token) via the shared ``delegated_auth``
78
+ helper from agent-utilities.
35
79
  """
80
+ instance, token, verify = _resolve_connection(instance, token, verify)
36
81
  from agent_utilities.mcp.delegated_auth import (
37
82
  get_delegated_token,
38
83
  get_user_identity,
@@ -80,15 +125,18 @@ def get_client(
80
125
 
81
126
 
82
127
  def get_graphql_client(
83
- instance: str = os.getenv("GITLAB_URL", "https://gitlab.com"),
84
- token: str | None = os.getenv("GITLAB_TOKEN", None),
85
- verify: bool = to_boolean(string=os.getenv("GITLAB_SSL_VERIFY", "True")),
128
+ instance: str | None = None,
129
+ token: str | None = None,
130
+ verify: bool | None = None,
86
131
  config: dict | None = None,
87
132
  ) -> Any:
88
133
  """Factory function to create the GitLab GraphQL client.
89
134
 
90
- Supports OIDC delegation and fixed credentials (token).
135
+ Multi-tenant (CONCEPT:KG-2.9g): ``instance`` selects a configured tenant by
136
+ name, a bare URL, or the default. Supports OIDC delegation and fixed
137
+ credentials (token).
91
138
  """
139
+ instance, token, verify = _resolve_connection(instance, token, verify)
92
140
  from agent_utilities.mcp.delegated_auth import (
93
141
  get_delegated_token,
94
142
  get_user_identity,
@@ -0,0 +1,110 @@
1
+ """Multi-tenant GitLab instance registry.
2
+
3
+ The single source of truth for *which* GitLab instances exist is the
4
+ agent-utilities XDG config (`~/.config/agent-utilities/config.json`,
5
+ `gitlab_instances`) — the same list the KG GitLab indexer reads — so one config
6
+ drives both code/metadata indexing AND every `gitlab-api` client/MCP call. When
7
+ no instances are configured it falls back to the single-host `GITLAB_URL` /
8
+ `GITLAB_TOKEN` env the connector has always used.
9
+
10
+ `get_client(instance="<name>")` (auth.py) resolves a configured instance by
11
+ name; a bare URL still works (back-compat), and an unset instance resolves to
12
+ the default (first configured, else `GITLAB_URL`).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ from dataclasses import dataclass
19
+ from urllib.parse import urlparse
20
+
21
+ from agent_utilities.base_utilities import get_logger, to_boolean
22
+
23
+ logger = get_logger(__name__)
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class GitLabInstance:
28
+ """Connection facts for one GitLab tenant."""
29
+
30
+ name: str
31
+ url: str
32
+ token: str = ""
33
+ verify_ssl: bool = True
34
+
35
+
36
+ def _host_slug(url: str) -> str:
37
+ return (urlparse(url).netloc or url).lower()
38
+
39
+
40
+ def _from_shared_config() -> list[GitLabInstance]:
41
+ """Read the structured `gitlab_instances` list from the agent-utilities config."""
42
+ try:
43
+ from agent_utilities.core.config import config
44
+
45
+ raw = getattr(config, "gitlab_instances", None) or []
46
+ except Exception: # noqa: BLE001 - config optional; fall back to env
47
+ return []
48
+ out: list[GitLabInstance] = []
49
+ for item in raw:
50
+ if not isinstance(item, dict):
51
+ continue
52
+ url = str(item.get("url", "")).strip()
53
+ if not url:
54
+ continue
55
+ out.append(
56
+ GitLabInstance(
57
+ name=str(item.get("name") or _host_slug(url)),
58
+ url=url,
59
+ token=str(item.get("token", "")),
60
+ verify_ssl=bool(item.get("verify_ssl", True)),
61
+ )
62
+ )
63
+ return out
64
+
65
+
66
+ def _single_host_fallback() -> list[GitLabInstance]:
67
+ """The legacy single-host instance from GITLAB_URL / GITLAB_TOKEN, if a token is set."""
68
+ url = os.getenv("GITLAB_URL", "https://gitlab.com")
69
+ token = os.getenv("GITLAB_TOKEN")
70
+ if not token:
71
+ return []
72
+ return [
73
+ GitLabInstance(
74
+ name=_host_slug(url),
75
+ url=url,
76
+ token=token,
77
+ verify_ssl=to_boolean(string=os.getenv("GITLAB_SSL_VERIFY", "True")),
78
+ )
79
+ ]
80
+
81
+
82
+ def list_configured_instances() -> list[GitLabInstance]:
83
+ """Every configured GitLab instance (shared config first, else single-host env)."""
84
+ return _from_shared_config() or _single_host_fallback()
85
+
86
+
87
+ def get_instance(name: str | None = None) -> GitLabInstance | None:
88
+ """Resolve one instance by name, or the default (first configured) when ``name`` is None."""
89
+ instances = list_configured_instances()
90
+ if not instances:
91
+ return None
92
+ if name is None:
93
+ return instances[0]
94
+ for inst in instances:
95
+ if inst.name == name:
96
+ return inst
97
+ return None
98
+
99
+
100
+ def instance_summaries() -> list[dict[str, object]]:
101
+ """Tenant list for discovery — names/urls/verify only, NEVER tokens."""
102
+ return [
103
+ {
104
+ "name": i.name,
105
+ "url": i.url,
106
+ "verify_ssl": i.verify_ssl,
107
+ "has_token": bool(i.token),
108
+ }
109
+ for i in list_configured_instances()
110
+ ]
@@ -5,6 +5,7 @@ Auto-generated from mcp_server.py during ecosystem standardization.
5
5
 
6
6
  from typing import Any
7
7
 
8
+ from agent_utilities.mcp_utilities import resolve_action, run_blocking
8
9
  from fastmcp import Context, FastMCP
9
10
  from fastmcp.dependencies import Depends
10
11
  from pydantic import Field
@@ -38,12 +39,19 @@ def register_branches_tools(mcp: FastMCP):
38
39
 
39
40
  kwargs = {k: v for k, v in kwargs.items() if v is not None}
40
41
 
42
+ resolved = resolve_action(
43
+ action, {"get", "create", "delete"}, service="gitlab-api"
44
+ )
45
+ if isinstance(resolved, dict):
46
+ return resolved
47
+ action = resolved
48
+
41
49
  if action == "get":
42
50
  if "branch" in kwargs:
43
- return client.get_branch(**kwargs)
44
- return client.get_branches(**kwargs)
51
+ return await run_blocking(client.get_branch, **kwargs)
52
+ return await run_blocking(client.get_branches, **kwargs)
45
53
  if action == "create":
46
- return client.create_branch(**kwargs)
54
+ return await run_blocking(client.create_branch, **kwargs)
47
55
  if action == "delete":
48
- return client.delete_branch(**kwargs)
56
+ return await run_blocking(client.delete_branch, **kwargs)
49
57
  raise ValueError(f"Unknown action: {action}")
@@ -5,6 +5,7 @@ Auto-generated from mcp_server.py during ecosystem standardization.
5
5
 
6
6
  from typing import Any
7
7
 
8
+ from agent_utilities.mcp_utilities import resolve_action, run_blocking
8
9
  from fastmcp import Context, FastMCP
9
10
  from fastmcp.dependencies import Depends
10
11
  from pydantic import Field
@@ -38,28 +39,49 @@ def register_commits_tools(mcp: FastMCP):
38
39
 
39
40
  kwargs = {k: v for k, v in kwargs.items() if v is not None}
40
41
 
42
+ resolved = resolve_action(
43
+ action,
44
+ {
45
+ "get",
46
+ "create",
47
+ "diff",
48
+ "revert",
49
+ "get_comments",
50
+ "create_comment",
51
+ "get_discussions",
52
+ "get_statuses",
53
+ "post_status",
54
+ "get_merge_requests",
55
+ "get_gpg_signature",
56
+ },
57
+ service="gitlab-api",
58
+ )
59
+ if isinstance(resolved, dict):
60
+ return resolved
61
+ action = resolved
62
+
41
63
  if action == "get":
42
64
  if "commit_sha" in kwargs:
43
- return client.get_commit(**kwargs)
44
- return client.get_commits(**kwargs)
65
+ return await run_blocking(client.get_commit, **kwargs)
66
+ return await run_blocking(client.get_commits, **kwargs)
45
67
  if action == "create":
46
- return client.create_commit(**kwargs)
68
+ return await run_blocking(client.create_commit, **kwargs)
47
69
  if action == "diff":
48
- return client.get_commit_diff(**kwargs)
70
+ return await run_blocking(client.get_commit_diff, **kwargs)
49
71
  if action == "revert":
50
- return client.revert_commit(**kwargs)
72
+ return await run_blocking(client.revert_commit, **kwargs)
51
73
  if action == "get_comments":
52
- return client.get_commit_comments(**kwargs)
74
+ return await run_blocking(client.get_commit_comments, **kwargs)
53
75
  if action == "create_comment":
54
- return client.create_commit_comment(**kwargs)
76
+ return await run_blocking(client.create_commit_comment, **kwargs)
55
77
  if action == "get_discussions":
56
- return client.get_commit_discussions(**kwargs)
78
+ return await run_blocking(client.get_commit_discussions, **kwargs)
57
79
  if action == "get_statuses":
58
- return client.get_commit_statuses(**kwargs)
80
+ return await run_blocking(client.get_commit_statuses, **kwargs)
59
81
  if action == "post_status":
60
- return client.post_build_status_to_commit(**kwargs)
82
+ return await run_blocking(client.post_build_status_to_commit, **kwargs)
61
83
  if action == "get_merge_requests":
62
- return client.get_commit_merge_requests(**kwargs)
84
+ return await run_blocking(client.get_commit_merge_requests, **kwargs)
63
85
  if action == "get_gpg_signature":
64
- return client.get_commit_gpg_signature(**kwargs)
86
+ return await run_blocking(client.get_commit_gpg_signature, **kwargs)
65
87
  raise ValueError(f"Unknown action: {action}")
@@ -5,6 +5,7 @@ Auto-generated from mcp_server.py during ecosystem standardization.
5
5
 
6
6
  from typing import Any
7
7
 
8
+ from agent_utilities.mcp_utilities import run_blocking
8
9
  from fastmcp import Context, FastMCP
9
10
  from fastmcp.dependencies import Depends
10
11
  from pydantic import Field
@@ -41,4 +42,6 @@ def register_custom_api_tools(mcp: FastMCP):
41
42
  return {"error": f"Invalid params_json: {e}"}
42
43
 
43
44
  kwargs = {k: v for k, v in kwargs.items() if v is not None}
44
- return client.api_request(method=method, endpoint=endpoint, **kwargs)
45
+ return await run_blocking(
46
+ client.api_request, method=method, endpoint=endpoint, **kwargs
47
+ )
@@ -5,6 +5,7 @@ Auto-generated from mcp_server.py during ecosystem standardization.
5
5
 
6
6
  from typing import Any
7
7
 
8
+ from agent_utilities.mcp_utilities import resolve_action, run_blocking
8
9
  from fastmcp import Context, FastMCP
9
10
  from fastmcp.dependencies import Depends
10
11
  from pydantic import Field
@@ -38,22 +39,39 @@ def register_deploy_tokens_tools(mcp: FastMCP):
38
39
 
39
40
  kwargs = {k: v for k, v in kwargs.items() if v is not None}
40
41
 
42
+ resolved = resolve_action(
43
+ action,
44
+ {
45
+ "get",
46
+ "get_project",
47
+ "create_project",
48
+ "delete_project",
49
+ "get_group",
50
+ "create_group",
51
+ "delete_group",
52
+ },
53
+ service="gitlab-api",
54
+ )
55
+ if isinstance(resolved, dict):
56
+ return resolved
57
+ action = resolved
58
+
41
59
  if action == "get":
42
60
  if "token_id" in kwargs and "project_id" in kwargs:
43
- return client.get_project_deploy_token(**kwargs)
61
+ return await run_blocking(client.get_project_deploy_token, **kwargs)
44
62
  elif "token_id" in kwargs and "group_id" in kwargs:
45
- return client.get_group_deploy_token(**kwargs)
46
- return client.get_deploy_tokens(**kwargs)
63
+ return await run_blocking(client.get_group_deploy_token, **kwargs)
64
+ return await run_blocking(client.get_deploy_tokens, **kwargs)
47
65
  if action == "get_project":
48
- return client.get_project_deploy_tokens(**kwargs)
66
+ return await run_blocking(client.get_project_deploy_tokens, **kwargs)
49
67
  if action == "create_project":
50
- return client.create_project_deploy_token(**kwargs)
68
+ return await run_blocking(client.create_project_deploy_token, **kwargs)
51
69
  if action == "delete_project":
52
- return client.delete_project_deploy_token(**kwargs)
70
+ return await run_blocking(client.delete_project_deploy_token, **kwargs)
53
71
  if action == "get_group":
54
- return client.get_group_deploy_tokens(**kwargs)
72
+ return await run_blocking(client.get_group_deploy_tokens, **kwargs)
55
73
  if action == "create_group":
56
- return client.create_group_deploy_token(**kwargs)
74
+ return await run_blocking(client.create_group_deploy_token, **kwargs)
57
75
  if action == "delete_group":
58
- return client.delete_group_deploy_token(**kwargs)
76
+ return await run_blocking(client.delete_group_deploy_token, **kwargs)
59
77
  raise ValueError(f"Unknown action: {action}")
@@ -5,6 +5,7 @@ Auto-generated from mcp_server.py during ecosystem standardization.
5
5
 
6
6
  from typing import Any
7
7
 
8
+ from agent_utilities.mcp_utilities import resolve_action, run_blocking
8
9
  from fastmcp import Context, FastMCP
9
10
  from fastmcp.dependencies import Depends
10
11
  from pydantic import Field
@@ -38,30 +39,51 @@ def register_environments_tools(mcp: FastMCP):
38
39
 
39
40
  kwargs = {k: v for k, v in kwargs.items() if v is not None}
40
41
 
42
+ resolved = resolve_action(
43
+ action,
44
+ {
45
+ "get",
46
+ "create",
47
+ "update",
48
+ "delete",
49
+ "stop",
50
+ "stop_stale",
51
+ "delete_stopped",
52
+ "get_protected",
53
+ "protect",
54
+ "update_protected",
55
+ "unprotect",
56
+ },
57
+ service="gitlab-api",
58
+ )
59
+ if isinstance(resolved, dict):
60
+ return resolved
61
+ action = resolved
62
+
41
63
  if action == "get":
42
64
  if "environment_id" in kwargs:
43
- return client.get_environment(**kwargs)
44
- return client.get_environments(**kwargs)
65
+ return await run_blocking(client.get_environment, **kwargs)
66
+ return await run_blocking(client.get_environments, **kwargs)
45
67
  if action == "create":
46
- return client.create_environment(**kwargs)
68
+ return await run_blocking(client.create_environment, **kwargs)
47
69
  if action == "update":
48
- return client.update_environment(**kwargs)
70
+ return await run_blocking(client.update_environment, **kwargs)
49
71
  if action == "delete":
50
- return client.delete_environment(**kwargs)
72
+ return await run_blocking(client.delete_environment, **kwargs)
51
73
  if action == "stop":
52
- return client.stop_environment(**kwargs)
74
+ return await run_blocking(client.stop_environment, **kwargs)
53
75
  if action == "stop_stale":
54
- return client.stop_stale_environments(**kwargs)
76
+ return await run_blocking(client.stop_stale_environments, **kwargs)
55
77
  if action == "delete_stopped":
56
- return client.delete_stopped_environments(**kwargs)
78
+ return await run_blocking(client.delete_stopped_environments, **kwargs)
57
79
  if action == "get_protected":
58
80
  if "environment_name" in kwargs:
59
- return client.get_protected_environment(**kwargs)
60
- return client.get_protected_environments(**kwargs)
81
+ return await run_blocking(client.get_protected_environment, **kwargs)
82
+ return await run_blocking(client.get_protected_environments, **kwargs)
61
83
  if action == "protect":
62
- return client.protect_environment(**kwargs)
84
+ return await run_blocking(client.protect_environment, **kwargs)
63
85
  if action == "update_protected":
64
- return client.update_protected_environment(**kwargs)
86
+ return await run_blocking(client.update_protected_environment, **kwargs)
65
87
  if action == "unprotect":
66
- return client.unprotect_environment(**kwargs)
88
+ return await run_blocking(client.unprotect_environment, **kwargs)
67
89
  raise ValueError(f"Unknown action: {action}")
@@ -5,6 +5,7 @@ Auto-generated from mcp_server.py during ecosystem standardization.
5
5
 
6
6
  from typing import Any
7
7
 
8
+ from agent_utilities.mcp_utilities import resolve_action, run_blocking
8
9
  from fastmcp import Context, FastMCP
9
10
  from fastmcp.dependencies import Depends
10
11
  from pydantic import Field
@@ -38,14 +39,21 @@ def register_epics_tools(mcp: FastMCP):
38
39
 
39
40
  kwargs = {k: v for k, v in kwargs.items() if v is not None}
40
41
 
42
+ resolved = resolve_action(
43
+ action, {"get", "create", "update", "delete"}, service="gitlab-api"
44
+ )
45
+ if isinstance(resolved, dict):
46
+ return resolved
47
+ action = resolved
48
+
41
49
  if action == "get":
42
50
  if "epic_iid" in kwargs or "epic_id" in kwargs:
43
- return client.get_epic(**kwargs)
44
- return client.get_epics(**kwargs)
51
+ return await run_blocking(client.get_epic, **kwargs)
52
+ return await run_blocking(client.get_epics, **kwargs)
45
53
  if action == "create":
46
- return client.create_epic(**kwargs)
54
+ return await run_blocking(client.create_epic, **kwargs)
47
55
  if action == "update":
48
- return client.update_epic(**kwargs)
56
+ return await run_blocking(client.update_epic, **kwargs)
49
57
  if action == "delete":
50
- return client.delete_epic(**kwargs)
58
+ return await run_blocking(client.delete_epic, **kwargs)
51
59
  raise ValueError(f"Unknown action: {action}")