mcp-argocd 0.1.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 (74) hide show
  1. mcp_argocd-0.1.0/.env.example +16 -0
  2. mcp_argocd-0.1.0/.github/dependabot.yml +24 -0
  3. mcp_argocd-0.1.0/.github/workflows/link-check.yml +23 -0
  4. mcp_argocd-0.1.0/.github/workflows/lint.yml +11 -0
  5. mcp_argocd-0.1.0/.github/workflows/publish.yml +49 -0
  6. mcp_argocd-0.1.0/.github/workflows/release.yml +308 -0
  7. mcp_argocd-0.1.0/.github/workflows/tests.yml +21 -0
  8. mcp_argocd-0.1.0/.gitignore +43 -0
  9. mcp_argocd-0.1.0/AGENTS.md +181 -0
  10. mcp_argocd-0.1.0/CHANGELOG.md +7 -0
  11. mcp_argocd-0.1.0/LICENSE +21 -0
  12. mcp_argocd-0.1.0/PKG-INFO +339 -0
  13. mcp_argocd-0.1.0/README.md +302 -0
  14. mcp_argocd-0.1.0/evaluations/eval.xml +67 -0
  15. mcp_argocd-0.1.0/gemini-extension.json +33 -0
  16. mcp_argocd-0.1.0/llms-full.txt +211 -0
  17. mcp_argocd-0.1.0/llms.txt +28 -0
  18. mcp_argocd-0.1.0/pyproject.toml +106 -0
  19. mcp_argocd-0.1.0/scripts/derive_artifacts.py +78 -0
  20. mcp_argocd-0.1.0/server.json +43 -0
  21. mcp_argocd-0.1.0/src/mcp_argocd/__init__.py +69 -0
  22. mcp_argocd-0.1.0/src/mcp_argocd/__main__.py +5 -0
  23. mcp_argocd-0.1.0/src/mcp_argocd/client.py +176 -0
  24. mcp_argocd-0.1.0/src/mcp_argocd/config.py +73 -0
  25. mcp_argocd-0.1.0/src/mcp_argocd/exceptions.py +52 -0
  26. mcp_argocd-0.1.0/src/mcp_argocd/py.typed +0 -0
  27. mcp_argocd-0.1.0/src/mcp_argocd/resources/api-token-setup.md +20 -0
  28. mcp_argocd-0.1.0/src/mcp_argocd/resources/applicationsets.md +10 -0
  29. mcp_argocd-0.1.0/src/mcp_argocd/resources/gitops-change-flow.md +8 -0
  30. mcp_argocd-0.1.0/src/mcp_argocd/resources/prompts/diagnose-sync-failure.md +9 -0
  31. mcp_argocd-0.1.0/src/mcp_argocd/resources/prompts/fleet-status.md +8 -0
  32. mcp_argocd-0.1.0/src/mcp_argocd/resources/prompts/inspect-applicationset.md +9 -0
  33. mcp_argocd-0.1.0/src/mcp_argocd/resources/prompts/review-drift.md +8 -0
  34. mcp_argocd-0.1.0/src/mcp_argocd/resources/prompts/rollback-application.md +9 -0
  35. mcp_argocd-0.1.0/src/mcp_argocd/resources/prompts/safe-sync.md +9 -0
  36. mcp_argocd-0.1.0/src/mcp_argocd/resources/prompts/triage-application.md +9 -0
  37. mcp_argocd-0.1.0/src/mcp_argocd/resources/rbac.md +10 -0
  38. mcp_argocd-0.1.0/src/mcp_argocd/resources/rollback.md +9 -0
  39. mcp_argocd-0.1.0/src/mcp_argocd/resources/status-triage.md +25 -0
  40. mcp_argocd-0.1.0/src/mcp_argocd/resources/sync-safety.md +17 -0
  41. mcp_argocd-0.1.0/src/mcp_argocd/servers/__init__.py +0 -0
  42. mcp_argocd-0.1.0/src/mcp_argocd/servers/_helpers.py +126 -0
  43. mcp_argocd-0.1.0/src/mcp_argocd/servers/argocd.py +2043 -0
  44. mcp_argocd-0.1.0/src/mcp_argocd/servers/prompts.py +104 -0
  45. mcp_argocd-0.1.0/src/mcp_argocd/servers/resources.py +109 -0
  46. mcp_argocd-0.1.0/tests/__init__.py +0 -0
  47. mcp_argocd-0.1.0/tests/conftest.py +82 -0
  48. mcp_argocd-0.1.0/tests/fixtures/README.md +61 -0
  49. mcp_argocd-0.1.0/tests/fixtures/application.json +173 -0
  50. mcp_argocd-0.1.0/tests/fixtures/application_list.json +320 -0
  51. mcp_argocd-0.1.0/tests/fixtures/applicationset.json +80 -0
  52. mcp_argocd-0.1.0/tests/fixtures/cluster.json +41 -0
  53. mcp_argocd-0.1.0/tests/fixtures/events.json +30 -0
  54. mcp_argocd-0.1.0/tests/fixtures/logs.ndjson +4 -0
  55. mcp_argocd-0.1.0/tests/fixtures/managed_resources.json +28 -0
  56. mcp_argocd-0.1.0/tests/fixtures/project.json +65 -0
  57. mcp_argocd-0.1.0/tests/fixtures/repository.json +24 -0
  58. mcp_argocd-0.1.0/tests/fixtures/resource_tree.json +87 -0
  59. mcp_argocd-0.1.0/tests/fixtures/version.json +12 -0
  60. mcp_argocd-0.1.0/tests/test_links.py +128 -0
  61. mcp_argocd-0.1.0/tests/unit/__init__.py +0 -0
  62. mcp_argocd-0.1.0/tests/unit/test_cli.py +52 -0
  63. mcp_argocd-0.1.0/tests/unit/test_client.py +210 -0
  64. mcp_argocd-0.1.0/tests/unit/test_config.py +122 -0
  65. mcp_argocd-0.1.0/tests/unit/test_derived_artifacts.py +30 -0
  66. mcp_argocd-0.1.0/tests/unit/test_doc_parity.py +81 -0
  67. mcp_argocd-0.1.0/tests/unit/test_exceptions.py +92 -0
  68. mcp_argocd-0.1.0/tests/unit/test_helpers.py +142 -0
  69. mcp_argocd-0.1.0/tests/unit/test_prompts.py +108 -0
  70. mcp_argocd-0.1.0/tests/unit/test_resources.py +69 -0
  71. mcp_argocd-0.1.0/tests/unit/test_server_assembly.py +69 -0
  72. mcp_argocd-0.1.0/tests/unit/test_tool_contract.py +342 -0
  73. mcp_argocd-0.1.0/tests/unit/test_tools.py +980 -0
  74. mcp_argocd-0.1.0/uv.lock +2303 -0
@@ -0,0 +1,16 @@
1
+ # Argo CD base URL, including any path prefix (required)
2
+ ARGOCD_URL=https://argocd.example.com
3
+
4
+ # Bearer token (required) — any of these work, checked in this order:
5
+ # - ARGOCD_TOKEN
6
+ # - ARGOCD_AUTH_TOKEN (the argocd CLI's name)
7
+ # - ARGOCD_API_TOKEN (the official server's name)
8
+ # Generate one: argocd account generate-token --account <name> --expires-in 24h
9
+ ARGOCD_TOKEN=
10
+
11
+ # Optional settings
12
+ # ARGOCD_READ_ONLY=false
13
+ # ARGOCD_TIMEOUT=30
14
+ # ARGOCD_SSL_VERIFY=true
15
+ # ARGOCD_INSECURE=false
16
+ # ARGOCD_APP_NAMESPACE=
@@ -0,0 +1,24 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: pip
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ day: monday
8
+ open-pull-requests-limit: 10
9
+ labels:
10
+ - dependencies
11
+ commit-message:
12
+ prefix: "chore(deps)"
13
+
14
+ - package-ecosystem: github-actions
15
+ directory: /
16
+ schedule:
17
+ interval: weekly
18
+ day: monday
19
+ open-pull-requests-limit: 5
20
+ labels:
21
+ - dependencies
22
+ - ci
23
+ commit-message:
24
+ prefix: "chore(ci)"
@@ -0,0 +1,23 @@
1
+ name: Link check
2
+
3
+ # Documentation links are checked on a schedule rather than per-push. They
4
+ # assert on third-party availability, so a transient failure at pypi.org or
5
+ # shields.io says nothing about the commit that triggered it -- it only reddens
6
+ # an unrelated PR. Weekly is often enough to catch real rot, and a failure here
7
+ # is information rather than a merge blocker.
8
+ on:
9
+ schedule:
10
+ - cron: "0 6 * * 1" # Mondays, 06:00 UTC
11
+ workflow_dispatch:
12
+
13
+ jobs:
14
+ links:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v7
18
+ - uses: astral-sh/setup-uv@v7
19
+ with:
20
+ python-version: "3.13"
21
+ - run: uv sync --all-extras
22
+ # -m network selects exactly what the default run excludes.
23
+ - run: uv run pytest -m network tests/test_links.py
@@ -0,0 +1,11 @@
1
+ name: Lint
2
+ on: [push, pull_request]
3
+ jobs:
4
+ lint:
5
+ runs-on: ubuntu-latest
6
+ steps:
7
+ - uses: actions/checkout@v7
8
+ - uses: astral-sh/setup-uv@v7
9
+ - run: uv sync
10
+ - run: uv run ruff check .
11
+ - run: uv run ruff format --check .
@@ -0,0 +1,49 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build-n-publish:
10
+ runs-on: ubuntu-latest
11
+ environment: pypi
12
+ permissions:
13
+ id-token: write
14
+ contents: write
15
+
16
+ steps:
17
+ - uses: actions/checkout@v7
18
+
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v7
21
+ with:
22
+ version: "latest"
23
+
24
+ - name: Set up Python
25
+ run: uv python install
26
+
27
+ - name: Build package
28
+ run: uv build
29
+
30
+ - name: Publish to PyPI
31
+ uses: pypa/gh-action-pypi-publish@release/v1
32
+
33
+ - name: Create GitHub Release
34
+ uses: softprops/action-gh-release@v3
35
+ with:
36
+ files: dist/*
37
+ generate_release_notes: true
38
+ env:
39
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40
+
41
+ - name: Install mcp-publisher
42
+ run: |
43
+ curl -sL https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_linux_amd64.tar.gz | tar xz -C /usr/local/bin mcp-publisher
44
+ mcp-publisher --help
45
+
46
+ - name: Publish to MCP Registry
47
+ run: |
48
+ mcp-publisher login github-oidc
49
+ mcp-publisher publish
@@ -0,0 +1,308 @@
1
+ name: Semantic Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ bump:
7
+ description: "Version bump type (ignored if version is set)"
8
+ required: true
9
+ type: choice
10
+ options:
11
+ - patch
12
+ - minor
13
+ - major
14
+ version:
15
+ description: "Explicit version (e.g. 0.7.0) — overrides bump"
16
+ required: false
17
+ type: string
18
+ default: ""
19
+ dry_run:
20
+ description: "Dry run (no tag/push)"
21
+ required: false
22
+ type: boolean
23
+ default: false
24
+
25
+ jobs:
26
+ release:
27
+ runs-on: ubuntu-latest
28
+ permissions:
29
+ contents: write
30
+
31
+ steps:
32
+ - uses: actions/checkout@v7
33
+ with:
34
+ fetch-depth: 0
35
+
36
+ - name: Install uv
37
+ uses: astral-sh/setup-uv@v7
38
+ with:
39
+ version: "latest"
40
+
41
+ - name: Set up Python
42
+ run: uv python install
43
+
44
+ - name: Get current version
45
+ id: current
46
+ run: |
47
+ version=$(grep '^version' pyproject.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
48
+ echo "version=$version" >> "$GITHUB_OUTPUT"
49
+ echo "Current version: $version"
50
+
51
+ - name: Compute next version
52
+ id: next
53
+ run: |
54
+ if [ -n "${{ inputs.version }}" ]; then
55
+ version="${{ inputs.version }}"
56
+ echo "Explicit version: $version"
57
+ else
58
+ IFS='.' read -r major minor patch <<< "${{ steps.current.outputs.version }}"
59
+ case "${{ inputs.bump }}" in
60
+ major) major=$((major + 1)); minor=0; patch=0 ;;
61
+ minor) minor=$((minor + 1)); patch=0 ;;
62
+ patch) patch=$((patch + 1)) ;;
63
+ esac
64
+ version="$major.$minor.$patch"
65
+ fi
66
+ echo "version=$version" >> "$GITHUB_OUTPUT"
67
+ echo "Next version: $version"
68
+
69
+ - name: Update pyproject.toml version
70
+ run: |
71
+ sed -i "s/^version = \".*\"/version = \"${{ steps.next.outputs.version }}\"/" pyproject.toml
72
+ cat pyproject.toml | grep '^version'
73
+
74
+ - name: Update llms-full.txt version
75
+ run: |
76
+ for f in llms-full.txt; do
77
+ [ -f "$f" ] && sed -i 's/^- \*\*Version\*\*: .*/- **Version**: ${{ steps.next.outputs.version }}/' "$f"
78
+ done
79
+
80
+ - name: Update server.json version
81
+ env:
82
+ NEXT_VERSION: ${{ steps.next.outputs.version }}
83
+ run: |
84
+ python3 - <<'PY'
85
+ import json
86
+ import os
87
+
88
+ version = os.environ["NEXT_VERSION"]
89
+
90
+ with open("server.json") as f:
91
+ data = json.load(f)
92
+ data["version"] = version
93
+ for pkg in data.get("packages", []):
94
+ pkg["version"] = version
95
+ with open("server.json", "w") as f:
96
+ json.dump(data, f, indent=2)
97
+ f.write("\n")
98
+ PY
99
+
100
+ - name: Derive gemini-extension.json and llms.txt
101
+ run: python scripts/derive_artifacts.py
102
+
103
+ - name: Regenerate uv.lock
104
+ run: uv lock
105
+
106
+ - name: Generate CHANGELOG.md
107
+ id: changelog
108
+ env:
109
+ NEXT_VERSION: ${{ steps.next.outputs.version }}
110
+ run: |
111
+ # Find previous tag
112
+ PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
113
+ if [ -z "$PREV_TAG" ]; then
114
+ RANGE="HEAD"
115
+ else
116
+ RANGE="${PREV_TAG}..HEAD"
117
+ fi
118
+
119
+ DATE=$(date -u +%Y-%m-%d)
120
+ ENTRY="## [${NEXT_VERSION}] - ${DATE}"$'\n'
121
+
122
+ # Collect commits grouped by type
123
+ # Classify on the SUBJECT only. `git log --grep` matches the whole
124
+ # commit message, so a wrapped body line beginning "test ..." filed the
125
+ # same commit under both "Bug Fixes" and "Tests".
126
+ ALL_COMMITS=$(git log "$RANGE" --pretty=format:"%s|%h" 2>/dev/null || true)
127
+ for type_label in "feat:Features" "fix:Bug Fixes" "refactor:Refactoring" "docs:Documentation" "perf:Performance" "test:Tests" "chore:Chores"; do
128
+ type="${type_label%%:*}"
129
+ label="${type_label#*:}"
130
+ commits=$(printf '%s\n' "$ALL_COMMITS" \
131
+ | { grep -E "^${type}(\(.*\))?!?:" || true; } \
132
+ | while IFS='|' read -r msg hash; do printf -- "- %s (%s)\n" "$msg" "$hash"; done)
133
+ if [ -n "$commits" ]; then
134
+ ENTRY+=$'\n'"### ${label}"$'\n'
135
+ ENTRY+="${commits}"$'\n'
136
+ fi
137
+ done
138
+
139
+ # Collect commits that don't match any conventional type
140
+ other=$(printf '%s\n' "$ALL_COMMITS" \
141
+ | { grep -v -E "^(feat|fix|refactor|docs|perf|test|chore)(\(.*\))?!?:" || true; } \
142
+ | while IFS='|' read -r msg hash; do
143
+ if [ -n "$msg" ]; then printf -- "- %s (%s)\n" "$msg" "$hash"; fi
144
+ done)
145
+ if [ -n "$other" ]; then
146
+ ENTRY+=$'\n'"### Other"$'\n'
147
+ ENTRY+="${other}"$'\n'
148
+ fi
149
+
150
+ # Prepend to existing CHANGELOG.md or create new
151
+ if [ -f CHANGELOG.md ]; then
152
+ # Insert new entry after the first line (# Changelog header)
153
+ {
154
+ head -1 CHANGELOG.md
155
+ echo ""
156
+ echo "$ENTRY"
157
+ tail -n +2 CHANGELOG.md
158
+ } > CHANGELOG.tmp
159
+ mv CHANGELOG.tmp CHANGELOG.md
160
+ else
161
+ {
162
+ echo "# Changelog"
163
+ echo ""
164
+ echo "$ENTRY"
165
+ } > CHANGELOG.md
166
+ fi
167
+
168
+ # Save entry for summary display
169
+ echo "$ENTRY" > /tmp/changelog-entry.txt
170
+ echo "Generated CHANGELOG.md entry for v${NEXT_VERSION}"
171
+
172
+ - name: Verify build
173
+ run: uv build
174
+
175
+ - name: Create release commit and tag via API
176
+ if: ${{ !inputs.dry_run }}
177
+ env:
178
+ GH_TOKEN: ${{ secrets.RELEASE_PAT }}
179
+ NEXT_VERSION: ${{ steps.next.outputs.version }}
180
+ run: |
181
+ MAIN_SHA=$(gh api repos/${{ github.repository }}/git/ref/heads/main --jq '.object.sha')
182
+ BASE_TREE=$(gh api repos/${{ github.repository }}/git/commits/$MAIN_SHA --jq '.tree.sha')
183
+
184
+ # Create blobs for all modified files (use temp files to avoid ARG_MAX limits on large files)
185
+ create_blob() {
186
+ local file="$1"
187
+ base64 -w 0 "$file" > /tmp/b64content.txt
188
+ jq -Rs '{"encoding":"base64","content":rtrimstr("\n")}' /tmp/b64content.txt > /tmp/blob.json
189
+ gh api repos/${{ github.repository }}/git/blobs \
190
+ -X POST --input /tmp/blob.json --jq '.sha'
191
+ }
192
+
193
+ PYPROJECT_BLOB=$(create_blob pyproject.toml)
194
+ UVLOCK_BLOB=$(create_blob uv.lock)
195
+ CHANGELOG_BLOB=$(create_blob CHANGELOG.md)
196
+ LLMS_BLOB=$(create_blob llms.txt)
197
+ LLMS_FULL_BLOB=$(create_blob llms-full.txt)
198
+ SERVER_BLOB=$(create_blob server.json)
199
+ GEMINI_BLOB=$(create_blob gemini-extension.json)
200
+
201
+ # Create tree with all release files
202
+ NEW_TREE=$(gh api repos/${{ github.repository }}/git/trees \
203
+ -X POST \
204
+ --input - <<EOF | jq -r '.sha'
205
+ {
206
+ "base_tree": "$BASE_TREE",
207
+ "tree": [
208
+ {
209
+ "path": "pyproject.toml",
210
+ "mode": "100644",
211
+ "type": "blob",
212
+ "sha": "$PYPROJECT_BLOB"
213
+ },
214
+ {
215
+ "path": "uv.lock",
216
+ "mode": "100644",
217
+ "type": "blob",
218
+ "sha": "$UVLOCK_BLOB"
219
+ },
220
+ {
221
+ "path": "CHANGELOG.md",
222
+ "mode": "100644",
223
+ "type": "blob",
224
+ "sha": "$CHANGELOG_BLOB"
225
+ },
226
+ {
227
+ "path": "llms.txt",
228
+ "mode": "100644",
229
+ "type": "blob",
230
+ "sha": "$LLMS_BLOB"
231
+ },
232
+ {
233
+ "path": "llms-full.txt",
234
+ "mode": "100644",
235
+ "type": "blob",
236
+ "sha": "$LLMS_FULL_BLOB"
237
+ },
238
+ {
239
+ "path": "server.json",
240
+ "mode": "100644",
241
+ "type": "blob",
242
+ "sha": "$SERVER_BLOB"
243
+ },
244
+ {
245
+ "path": "gemini-extension.json",
246
+ "mode": "100644",
247
+ "type": "blob",
248
+ "sha": "$GEMINI_BLOB"
249
+ }
250
+ ]
251
+ }
252
+ EOF
253
+ )
254
+
255
+ # Create the commit
256
+ COMMIT_SHA=$(gh api repos/${{ github.repository }}/git/commits \
257
+ -X POST \
258
+ --input - <<EOF | jq -r '.sha'
259
+ {
260
+ "message": "chore(release): $NEXT_VERSION",
261
+ "tree": "$NEW_TREE",
262
+ "parents": ["$MAIN_SHA"],
263
+ "author": {
264
+ "name": "github-actions[bot]",
265
+ "email": "github-actions[bot]@users.noreply.github.com"
266
+ }
267
+ }
268
+ EOF
269
+ )
270
+
271
+ # Update main to point to the new commit
272
+ gh api repos/${{ github.repository }}/git/refs/heads/main \
273
+ -X PATCH \
274
+ -f sha="$COMMIT_SHA" \
275
+ -F force=false
276
+
277
+ # Create the tag reference
278
+ gh api repos/${{ github.repository }}/git/refs \
279
+ -X POST \
280
+ -f ref="refs/tags/v$NEXT_VERSION" \
281
+ -f sha="$COMMIT_SHA"
282
+
283
+ echo "Created commit $COMMIT_SHA and tag v$NEXT_VERSION"
284
+
285
+ - name: Dry run summary
286
+ if: ${{ inputs.dry_run }}
287
+ run: |
288
+ echo "## Dry Run Summary" >> "$GITHUB_STEP_SUMMARY"
289
+ echo "- Current: ${{ steps.current.outputs.version }}" >> "$GITHUB_STEP_SUMMARY"
290
+ echo "- Next: ${{ steps.next.outputs.version }}" >> "$GITHUB_STEP_SUMMARY"
291
+ echo "- Bump: ${{ inputs.bump }}" >> "$GITHUB_STEP_SUMMARY"
292
+ echo "- No changes pushed." >> "$GITHUB_STEP_SUMMARY"
293
+ echo "" >> "$GITHUB_STEP_SUMMARY"
294
+ echo "### Changelog Preview" >> "$GITHUB_STEP_SUMMARY"
295
+ echo '```' >> "$GITHUB_STEP_SUMMARY"
296
+ cat /tmp/changelog-entry.txt >> "$GITHUB_STEP_SUMMARY"
297
+ echo '```' >> "$GITHUB_STEP_SUMMARY"
298
+
299
+ - name: Release summary
300
+ if: ${{ !inputs.dry_run }}
301
+ run: |
302
+ echo "## Release v${{ steps.next.outputs.version }}" >> "$GITHUB_STEP_SUMMARY"
303
+ echo "- Tag pushed. Publish workflow will deploy to PyPI." >> "$GITHUB_STEP_SUMMARY"
304
+ echo "" >> "$GITHUB_STEP_SUMMARY"
305
+ echo "### Changelog Entry" >> "$GITHUB_STEP_SUMMARY"
306
+ echo '```' >> "$GITHUB_STEP_SUMMARY"
307
+ cat /tmp/changelog-entry.txt >> "$GITHUB_STEP_SUMMARY"
308
+ echo '```' >> "$GITHUB_STEP_SUMMARY"
@@ -0,0 +1,21 @@
1
+ name: Tests
2
+ on: [push, pull_request]
3
+ jobs:
4
+ test:
5
+ runs-on: ubuntu-latest
6
+ strategy:
7
+ matrix:
8
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
9
+ steps:
10
+ - uses: actions/checkout@v7
11
+ - uses: astral-sh/setup-uv@v7
12
+ with:
13
+ python-version: ${{ matrix.python-version }}
14
+ - run: uv sync --all-extras
15
+ - run: uv run pytest --cov --cov-report=xml --cov-fail-under=98
16
+ - name: Upload coverage
17
+ if: matrix.python-version == '3.13'
18
+ uses: codecov/codecov-action@v7
19
+ with:
20
+ file: coverage.xml
21
+ continue-on-error: true
@@ -0,0 +1,43 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ *.egg
9
+ .eggs/
10
+ .venv/
11
+ venv/
12
+ env/
13
+ .env
14
+ .env.local
15
+ *.log
16
+ .mypy_cache/
17
+ .pytest_cache/
18
+ .ruff_cache/
19
+ htmlcov/
20
+ .coverage
21
+ .coverage.*
22
+ coverage.xml
23
+ *.cover
24
+ .tox/
25
+ .nox/
26
+ .hypothesis/
27
+ *.swp
28
+ *.swo
29
+ *~
30
+ .DS_Store
31
+ Thumbs.db
32
+ .idea/
33
+ .vscode/
34
+ *.iml
35
+ .claude/
36
+ .cursorrules
37
+ .cursor/
38
+
39
+ # 1Password shell-plugin config: local tool state, not project config.
40
+ .op/
41
+
42
+ # Internal ticket tracker and spec; never committed.
43
+ .internal/
@@ -0,0 +1,181 @@
1
+ # mcp-argocd — Agent Context
2
+
3
+ MCP server exposing 37 tools, 7 resources, and 7 prompts over the Argo CD REST API. Read-mostly
4
+ operations for Argo CD 3.x: application status, sync, rollback, drift, logs, ApplicationSets,
5
+ clusters, projects, and repositories. Needs an API token; no cluster access.
6
+
7
+ ## Protocol Support
8
+
9
+ Supports the MCP 2026-07-28 specification (MCP 2.0) and stays compatible with 2025-11-25 clients.
10
+ Built on FastMCP 4.x and the MCP Python SDK 2.x. Verified over `stdio` and `streamable-http`.
11
+
12
+ Transports: `stdio` (default), `streamable-http` (recommended for remote), and `sse`. The
13
+ 2026-07-28 specification deprecates `sse`, so the server prints a warning when you use it. The
14
+ server uses no roots, sampling, logging, elicitation, or resource subscriptions, so the 2026-07-28
15
+ deprecations do not affect it.
16
+
17
+ ## Architecture
18
+
19
+ - **Entry point**: `src/mcp_argocd/__init__.py` — click CLI, loads `.env`, runs the FastMCP server
20
+ - **Client**: `src/mcp_argocd/client.py` — async httpx client; path-prefix-safe `{url}/api/v1` base with `/api/version` requested absolutely; grpc-gateway error mapping; `stream_lines` for logs
21
+ - **Tools**: `src/mcp_argocd/servers/argocd.py` — all 37 FastMCP tool registrations and the `_slim_*` functions
22
+ - **Resources**: `src/mcp_argocd/servers/resources.py` — 7 MCP resources (4 `resource://rules/*`, 3 `resource://guides/*`), content in `src/mcp_argocd/resources/*.md`
23
+ - **Prompts**: `src/mcp_argocd/servers/prompts.py` — 7 MCP prompts (multi-tool workflows)
24
+ - **Helpers**: `src/mcp_argocd/servers/_helpers.py` — file loader with traversal guard, `_scrub` over one `SECRET_KEYS` tuple, `_page`, `_split_app_name`, `_parse_resource_selector`, `_truncate`, `_terminal`
25
+ - **Config**: `src/mcp_argocd/config.py` — `ArgoCDConfig` built from env vars
26
+ - **Exceptions**: `src/mcp_argocd/exceptions.py` — `ArgoCDError` base; `ArgoCDApiError` (with `grpc_code`), `ArgoCDAuthError`, `ArgoCDNotFoundError`, `ArgoCDConflictError`, `ArgoCDWriteDisabledError`, `ArgoCDTimeoutError`
27
+ - **Tests**: `tests/` — `unit/test_tools.py`, `test_tool_contract.py`, `test_client.py`, `test_config.py`, `test_exceptions.py`, `test_helpers.py`, `test_server_assembly.py`, `test_resources.py`, `test_prompts.py`, `test_doc_parity.py`, `test_derived_artifacts.py`, plus `tests/test_links.py`. Fixtures in `tests/fixtures/` and shared fixtures in `tests/conftest.py`
28
+
29
+ ## Development
30
+
31
+ ```bash
32
+ uv sync --all-extras
33
+ uv run pytest --cov
34
+ uv run ruff check .
35
+ uv run ruff format --check . # CI runs --check; formatting drift fails the build
36
+ ```
37
+
38
+ `pytest` uses `asyncio_mode = "auto"` — async tests need no marker. Ruff line length is 100,
39
+ target py310; lint rules include `S` (bandit), `EM`, `N`, `UP`. `tests/**` waives `S101`,
40
+ `S105`, `S106`.
41
+
42
+ ## Patterns
43
+
44
+ - All tools are `async def` returning JSON strings
45
+ - `_ok(data)` for success, `_err(e)` for failure, `_paginated(items, total, has_more, next_offset)` for lists; `_ok_scrubbed` for repository/cluster/project payloads
46
+ - `_slim_*` functions trim each payload type; `get_*` tools expose `full=True` for the raw payload
47
+ - Write access control: `@tool_result(write=True)` runs `_check_write(ctx)`, which raises `ArgoCDWriteDisabledError` when `ARGOCD_READ_ONLY=true`
48
+ - Client-side paging: Argo CD list endpoints take no paging parameters, so the server fetches once, filters, sorts by name, and slices
49
+ - `namespace/name` handling: `_split_app_name` splits apps-in-any-namespace names into `name` + `appNamespace`
50
+ - Tags: every tool tagged `{"argocd", "<category>", "read"|"write"}`
51
+ - Parameters use `Annotated[type, Field(description=...)]`; `Literal[...]` for closed sets
52
+
53
+ ## MCP Compliance Rules
54
+
55
+ ### Tool annotations (mandatory)
56
+ Every tool MUST have `annotations={}` with at minimum `readOnlyHint`.
57
+ - Read tools: `annotations={"readOnlyHint": True, "idempotentHint": True, "openWorldHint": True}`
58
+ - Non-destructive writes: `destructiveHint: False`, `readOnlyHint: False`
59
+ - Destructive writes: `destructiveHint: True`, `readOnlyHint: False`
60
+ - Idempotent writes: add `idempotentHint: True`
61
+
62
+ ### Tool descriptions
63
+ 1-2 sentences. Front-load what it does AND what it returns.
64
+
65
+ ### Error handling
66
+ - Every tool wraps in `@tool_result`; expected `ArgoCDError` failures become the `_err` JSON envelope, anything else surfaces as a `ToolError` with a logged traceback.
67
+ - Error text MUST be actionable: what went wrong plus a suggested fix.
68
+ - Never expose the token, stack traces, or request headers.
69
+
70
+ ### Parameter design
71
+ - `Annotated[type, Field(description="...")]` on every parameter; a default on every optional.
72
+ - `Literal[...]` for known value sets; flatten — no nested dicts unless necessary.
73
+
74
+ ### Read-only mode
75
+ Every write tool MUST call `_check_write(ctx)` (via `@tool_result(write=True)`) before any mutation.
76
+
77
+ ## Naming verbs
78
+
79
+ `list`, `get`, `sync`, `rollback`, `terminate`, `create`, `patch`, `delete`, `run`, `invalidate`,
80
+ `generate`, `wait`, `can`.
81
+
82
+ ## Tool Categories (37)
83
+
84
+ | Category | Count | Operations |
85
+ |---|---|---|
86
+ | Applications — read | 14 | list, get, resource-tree, managed-resources, resource, manifests, events, logs, history, revision-metadata, operation, wait, sync-windows, resource-actions |
87
+ | Applications — write | 8 | sync, rollback, terminate, create, patch, delete, run action, delete resource |
88
+ | ApplicationSets | 3 | list, get, generate (dry-run) |
89
+ | Projects | 2 | list, get |
90
+ | Clusters | 3 | list, get, invalidate cache |
91
+ | Repositories | 3 | list, refs, apps |
92
+ | Server and account | 4 | version, userinfo, can-i, settings |
93
+
94
+ ## Common Workflows
95
+
96
+ - **Status triage**: `argocd_get_application` → `argocd_get_resource_tree` → `argocd_get_application_events` → `argocd_get_pod_logs`
97
+ - **Diagnose sync failure**: `argocd_get_operation` → `argocd_get_application_events` → `argocd_get_managed_resources`
98
+ - **Review drift**: `argocd_list_applications(sync_status=OutOfSync)` → `argocd_get_managed_resources`
99
+ - **Safe sync**: `argocd_get_sync_windows` → `argocd_sync_application(dry_run=True)` → `argocd_sync_application(wait=True)` → `argocd_get_application`
100
+ - **Rollback**: `argocd_get_application_history` → `argocd_get_revision_metadata` → `argocd_rollback_application(wait=True)`
101
+ - **Fleet status**: `argocd_list_clusters` → `argocd_list_applications`
102
+ - **Inspect ApplicationSet**: `argocd_get_applicationset` → `argocd_generate_applicationset`
103
+
104
+ ## Prompts
105
+
106
+ Prompt content lives as `.md` files in `src/mcp_argocd/resources/prompts/`. `servers/prompts.py`
107
+ loads each via `string.Template.safe_substitute` and registers it with `@mcp.prompt()`. Each
108
+ returns a user message (workflow template) plus an assistant acknowledgment.
109
+
110
+ | Prompt | Purpose | Tags |
111
+ |---|---|---|
112
+ | `triage_application` | Diagnose a Degraded/OutOfSync app | argocd, triage |
113
+ | `diagnose_sync_failure` | Classify a sync failure | argocd, sync |
114
+ | `review_drift` | Review OutOfSync apps and diffs | argocd, drift |
115
+ | `safe_sync` | Dry-run then sync and wait | argocd, sync |
116
+ | `rollback_application` | Roll back with the auto-sync check | argocd, rollback |
117
+ | `fleet_status` | Cluster and app health across a fleet | argocd, fleet |
118
+ | `inspect_applicationset` | Compare generated vs existing apps | argocd, applicationset |
119
+
120
+ ## Environment Variables
121
+
122
+ | Variable | Required | Default | Notes |
123
+ |---|---|---|---|
124
+ | `ARGOCD_URL` | yes | — | Base URL including any path prefix; trailing slash stripped; `/api/v1` appended. Falls back to `ARGOCD_SERVER` |
125
+ | `ARGOCD_TOKEN` | yes | — | Bearer token; also reads `ARGOCD_AUTH_TOKEN`, `ARGOCD_API_TOKEN` |
126
+ | `ARGOCD_READ_ONLY` | no | `false` | `true`/`1`/`yes` blocks the 9 write tools before any API call |
127
+ | `ARGOCD_TIMEOUT` | no | `30` | Request timeout in seconds |
128
+ | `ARGOCD_SSL_VERIFY` | no | `true` | `false`/`0`/`no` skips verification; `ARGOCD_INSECURE=true` is an alias |
129
+ | `ARGOCD_APP_NAMESPACE` | no | — | Default `appNamespace` for apps-in-any-namespace installs |
130
+
131
+ CLI flags override env: `--argocd-url`, `--argocd-token`, `--read-only`, `--insecure`, plus
132
+ `--transport {stdio,sse,streamable-http}` with `--host` and `--port` for non-stdio transports.
133
+
134
+ ## Release Workflow
135
+
136
+ Releases run through GitHub Actions — never bump versions manually.
137
+
138
+ ```bash
139
+ gh workflow run release.yml -f version=0.1.0
140
+ gh workflow run release.yml -f bump=minor -f dry_run=true # preview changelog, no push
141
+ ```
142
+
143
+ 1. `release.yml` (workflow_dispatch) — bumps the version in `pyproject.toml`, `llms-full.txt`,
144
+ `server.json`; runs `scripts/derive_artifacts.py`; regenerates `uv.lock`; prepends a CHANGELOG
145
+ entry; creates the release commit and tag via the GitHub API with `RELEASE_PAT`.
146
+ 2. `publish.yml` (on a `v*` tag) — builds the wheel, publishes to PyPI (trusted publishing,
147
+ environment `pypi`), creates the GitHub Release, then publishes to the MCP Registry.
148
+
149
+ Rules: never edit the `pyproject.toml` version directly; never create tags manually; use
150
+ Conventional Commits (`feat:`, `fix:`, `docs:`, …).
151
+
152
+ ## Documentation Freshness (mandatory)
153
+
154
+ When a changeset adds, removes, or modifies tools, resources, or prompts, update ALL of these in
155
+ the same commit:
156
+
157
+ - `README.md` — counts in heading and intro, tool table, full tool reference, usage examples, permissions
158
+ - `llms.txt` — count in tagline and documentation link (derived from `llms-full.txt`)
159
+ - `llms-full.txt` — count in tagline, documentation link, full tool reference
160
+ - `AGENTS.md` — counts in intro, tool category table
161
+ - `server.json` — description field (≤100 chars)
162
+
163
+ `llms.txt` and `gemini-extension.json` are derived — run `python scripts/derive_artifacts.py`,
164
+ never hand-edit them.
165
+
166
+ ## Known Limitations
167
+
168
+ - 37 tools in one server file, past the 5-15 guideline. If you refactor it, split by category.
169
+ - Errors come back as successful tool results carrying `{"error": ...}` (soft-error pattern);
170
+ callers inspect the JSON rather than relying on protocol-level errors.
171
+ - Argo CD list endpoints have no server-side paging, so a very large install pays one full fetch
172
+ per list call; the server pages the result client-side.
173
+ - Pod logs are capped at 1000 lines.
174
+
175
+ ## Future work
176
+
177
+ - **OAuth 2.1 / identity passthrough** is out of scope for 0.1.0 (spec decision D6). A design
178
+ exists at `.internal/specs/2026-10-03-oauth21.md`; when it lands it applies to all four sibling
179
+ servers at once. Until then, use an API-key account or a project-role token.
180
+ - **Session login** (`ARGOCD_USERNAME`/`ARGOCD_PASSWORD` → `POST /api/v1/session`) is deferred: it
181
+ works only for local accounts with the `login` capability, never SSO, and tokens expire in 24h.