dxe-mcp 0.0.1__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.
- dxe_mcp-0.0.1/.github/workflows/ci.yml +92 -0
- dxe_mcp-0.0.1/.github/workflows/publish.yml +246 -0
- dxe_mcp-0.0.1/.gitignore +8 -0
- dxe_mcp-0.0.1/CHANGELOG.md +16 -0
- dxe_mcp-0.0.1/LICENSE +674 -0
- dxe_mcp-0.0.1/PKG-INFO +128 -0
- dxe_mcp-0.0.1/README.md +103 -0
- dxe_mcp-0.0.1/pyproject.toml +63 -0
- dxe_mcp-0.0.1/server.json +26 -0
- dxe_mcp-0.0.1/src/dxe_mcp/__init__.py +9 -0
- dxe_mcp-0.0.1/src/dxe_mcp/server.py +72 -0
- dxe_mcp-0.0.1/tests/test_registry_metadata.py +41 -0
- dxe_mcp-0.0.1/tests/test_security.py +81 -0
- dxe_mcp-0.0.1/tests/test_tools.py +26 -0
- dxe_mcp-0.0.1/uv.lock +2478 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [main, develop]
|
|
6
|
+
push:
|
|
7
|
+
branches: [main, develop]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
# main takes a release PR from develop, or a security branch, and nothing else:
|
|
18
|
+
# merging into main publishes. Required on main by its branch protection.
|
|
19
|
+
release-source:
|
|
20
|
+
name: Release PR source
|
|
21
|
+
if: github.event_name == 'pull_request' && github.base_ref == 'main'
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- run: |
|
|
25
|
+
case "${{ github.head_ref }}" in
|
|
26
|
+
develop|security/*) echo "ok: ${{ github.head_ref }} -> main" ;;
|
|
27
|
+
*) echo "main takes a release PR from develop, or a security/ branch"; exit 1 ;;
|
|
28
|
+
esac
|
|
29
|
+
|
|
30
|
+
# Lint and types, once rather than per Python: both read the source, and
|
|
31
|
+
# neither answer changes with the interpreter running them (qso-graph-devel#66).
|
|
32
|
+
lint:
|
|
33
|
+
name: ruff and mypy
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v5
|
|
37
|
+
|
|
38
|
+
- uses: astral-sh/setup-uv@v6
|
|
39
|
+
with:
|
|
40
|
+
python-version: "3.11"
|
|
41
|
+
enable-cache: true
|
|
42
|
+
|
|
43
|
+
- name: Install (locked)
|
|
44
|
+
run: uv sync --group dev --frozen
|
|
45
|
+
|
|
46
|
+
- name: Lint (ruff)
|
|
47
|
+
run: uv run ruff check .
|
|
48
|
+
|
|
49
|
+
- name: Type-check (mypy)
|
|
50
|
+
run: uv run mypy src
|
|
51
|
+
|
|
52
|
+
test:
|
|
53
|
+
name: pytest (${{ matrix.python-version }})
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
strategy:
|
|
56
|
+
fail-fast: false
|
|
57
|
+
matrix:
|
|
58
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
59
|
+
steps:
|
|
60
|
+
- uses: actions/checkout@v5
|
|
61
|
+
|
|
62
|
+
- uses: astral-sh/setup-uv@v6
|
|
63
|
+
with:
|
|
64
|
+
python-version: ${{ matrix.python-version }}
|
|
65
|
+
enable-cache: true
|
|
66
|
+
|
|
67
|
+
- name: Install (locked)
|
|
68
|
+
run: uv sync --group dev --frozen
|
|
69
|
+
|
|
70
|
+
- name: Unit tests
|
|
71
|
+
run: uv run pytest tests --ignore=tests/test_security.py -v
|
|
72
|
+
|
|
73
|
+
- name: Security tests
|
|
74
|
+
run: uv run pytest tests/test_security.py -v
|
|
75
|
+
|
|
76
|
+
ci-all-green:
|
|
77
|
+
name: ci-all-green
|
|
78
|
+
needs: [lint, test]
|
|
79
|
+
runs-on: ubuntu-latest
|
|
80
|
+
if: always()
|
|
81
|
+
steps:
|
|
82
|
+
- name: Verify required jobs all succeeded
|
|
83
|
+
run: |
|
|
84
|
+
if [[ "${{ needs.lint.result }}" != "success" ]]; then
|
|
85
|
+
echo "FAIL: ruff or mypy did not pass (result=${{ needs.lint.result }})"
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
if [[ "${{ needs.test.result }}" != "success" ]]; then
|
|
89
|
+
echo "FAIL: test matrix did not all succeed (result=${{ needs.test.result }})"
|
|
90
|
+
exit 1
|
|
91
|
+
fi
|
|
92
|
+
echo "All required CI jobs passed."
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# The release flow (qso-graph/.github TEMPLATES.md): work lands on `develop`; a release is a
|
|
4
|
+
# PR from `develop` into `main`, and merging it publishes. `main` is always the released code.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: publish
|
|
11
|
+
cancel-in-progress: false
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
security:
|
|
18
|
+
name: Security gates
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
outputs:
|
|
21
|
+
version: ${{ steps.version.outputs.version }}
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
# main changes only by a release PR, so main's version must be new. PyPI publishes
|
|
26
|
+
# pyproject's version and the Registry server.json's, so they must agree too.
|
|
27
|
+
# Computed once; every later job uses this value (needs.security.outputs.version).
|
|
28
|
+
# The gate fails closed: anything it cannot confirm stops the release.
|
|
29
|
+
- name: The version on main is new and consistent
|
|
30
|
+
id: version
|
|
31
|
+
run: |
|
|
32
|
+
VERSION="$(grep -m1 '^version' pyproject.toml | cut -d'"' -f2)"
|
|
33
|
+
PACKAGE="$(jq -r '.packages[0].identifier' server.json)"
|
|
34
|
+
echo "pyproject=${VERSION} server.json=$(jq -r '.version' server.json)/$(jq -r '.packages[0].version' server.json)"
|
|
35
|
+
if [ "$(jq -r '.version' server.json)" != "$VERSION" ] || [ "$(jq -r '.packages[0].version' server.json)" != "$VERSION" ]; then
|
|
36
|
+
echo "FAIL: server.json does not match pyproject.toml version ${VERSION}"
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
# Only a 404 means "not published yet"; unreachable or 5xx is not a yes.
|
|
40
|
+
CODE="$(curl -s -o /dev/null -w '%{http_code}' "https://pypi.org/pypi/${PACKAGE}/${VERSION}/json")"
|
|
41
|
+
case "$CODE" in
|
|
42
|
+
404) echo "PyPI: ${PACKAGE} ${VERSION} is new" ;;
|
|
43
|
+
200) echo "FAIL: ${PACKAGE} ${VERSION} is already on PyPI. main changes only by a release PR with a new version."; exit 1 ;;
|
|
44
|
+
*) echo "FAIL: PyPI returned ${CODE}; cannot confirm ${VERSION} is new"; exit 1 ;;
|
|
45
|
+
esac
|
|
46
|
+
# git ls-remote --exit-code: 2 means no such tag; anything else (0, 128) stops.
|
|
47
|
+
set +e
|
|
48
|
+
git ls-remote --exit-code --tags origin "refs/tags/v${VERSION}" > /dev/null
|
|
49
|
+
RC=$?
|
|
50
|
+
set -e
|
|
51
|
+
case "$RC" in
|
|
52
|
+
2) echo "tag v${VERSION} does not exist yet" ;;
|
|
53
|
+
0) echo "FAIL: tag v${VERSION} already exists"; exit 1 ;;
|
|
54
|
+
*) echo "FAIL: could not check tags (git ls-remote exited ${RC})"; exit 1 ;;
|
|
55
|
+
esac
|
|
56
|
+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
- uses: actions/setup-python@v5
|
|
61
|
+
with:
|
|
62
|
+
python-version: "3.12"
|
|
63
|
+
|
|
64
|
+
- name: Install test dependencies
|
|
65
|
+
run: pip install pytest pip-audit
|
|
66
|
+
|
|
67
|
+
- name: Run security test suite
|
|
68
|
+
run: python -m pytest tests/test_security.py -v
|
|
69
|
+
|
|
70
|
+
- name: Audit dependencies for vulnerabilities
|
|
71
|
+
# Audits this package's declared dependencies, and can fail: a known CVE stops the release.
|
|
72
|
+
# A finding with no fix yet is excepted only explicitly: --ignore-vuln ID, with a reason and date.
|
|
73
|
+
run: pip-audit --strict --desc on .
|
|
74
|
+
|
|
75
|
+
- name: Grep check — no subprocess/shell
|
|
76
|
+
run: |
|
|
77
|
+
if grep -rn 'subprocess\.\|os\.system\|shell=True' src/; then
|
|
78
|
+
echo "FAIL: subprocess/shell found in source"
|
|
79
|
+
exit 1
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
- name: Grep check — no plaintext http://
|
|
83
|
+
run: |
|
|
84
|
+
if grep -rn 'http://' src/ | grep -v 'localhost\|127\.0\.0\.1\|::1'; then
|
|
85
|
+
echo "FAIL: non-HTTPS URL found in source"
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
- name: Grep check — no credential logging
|
|
90
|
+
run: |
|
|
91
|
+
if grep -rn -i 'print.*password\|print.*api_key\|logging.*password\|logging.*api_key' src/; then
|
|
92
|
+
echo "FAIL: credential logging found in source"
|
|
93
|
+
exit 1
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
publish:
|
|
97
|
+
name: Build and publish to PyPI
|
|
98
|
+
needs: security
|
|
99
|
+
runs-on: ubuntu-latest
|
|
100
|
+
environment: pypi
|
|
101
|
+
permissions:
|
|
102
|
+
id-token: write # Required for trusted publishing (OIDC)
|
|
103
|
+
steps:
|
|
104
|
+
- uses: actions/checkout@v4
|
|
105
|
+
|
|
106
|
+
- uses: actions/setup-python@v5
|
|
107
|
+
with:
|
|
108
|
+
python-version: "3.12"
|
|
109
|
+
|
|
110
|
+
- name: Install build dependencies
|
|
111
|
+
run: pip install build
|
|
112
|
+
|
|
113
|
+
- name: Build package
|
|
114
|
+
run: python -m build
|
|
115
|
+
|
|
116
|
+
- name: Publish to PyPI
|
|
117
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# Registry-publish — pushes server.json to the Official MCP Registry after
|
|
121
|
+
# PyPI publish succeeds, so discovery surfaces stay in sync with PyPI.
|
|
122
|
+
# Canonical template: https://github.com/qso-graph/.github/blob/main/TEMPLATES.md
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
registry-publish:
|
|
125
|
+
name: Publish to MCP Registry
|
|
126
|
+
needs: [security, publish] # waits for PyPI publish to succeed
|
|
127
|
+
runs-on: ubuntu-latest
|
|
128
|
+
permissions:
|
|
129
|
+
id-token: write # GitHub OIDC
|
|
130
|
+
contents: read
|
|
131
|
+
|
|
132
|
+
steps:
|
|
133
|
+
- uses: actions/checkout@v5
|
|
134
|
+
|
|
135
|
+
- name: Install jq (server.json bump)
|
|
136
|
+
run: sudo apt-get update && sudo apt-get install -y jq
|
|
137
|
+
|
|
138
|
+
- name: Install mcp-publisher
|
|
139
|
+
run: |
|
|
140
|
+
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
|
|
141
|
+
./mcp-publisher --help > /dev/null # smoke test
|
|
142
|
+
|
|
143
|
+
- name: Set server.json to the released version
|
|
144
|
+
run: |
|
|
145
|
+
VERSION="${{ needs.security.outputs.version }}"
|
|
146
|
+
echo "Setting server.json to ${VERSION}"
|
|
147
|
+
jq --arg v "$VERSION" \
|
|
148
|
+
'.version = $v | .packages[0].version = $v' \
|
|
149
|
+
server.json > /tmp/server.json
|
|
150
|
+
mv /tmp/server.json server.json
|
|
151
|
+
cat server.json
|
|
152
|
+
|
|
153
|
+
# The Registry checks PyPI itself and refuses a version PyPI isn't
|
|
154
|
+
# serving yet, which can lag the upload by a minute or more.
|
|
155
|
+
- name: Wait until PyPI serves this version
|
|
156
|
+
run: |
|
|
157
|
+
VERSION="${{ needs.security.outputs.version }}"
|
|
158
|
+
PACKAGE="$(jq -r '.packages[0].identifier' server.json)"
|
|
159
|
+
for i in $(seq 1 40); do
|
|
160
|
+
if curl -fsS "https://pypi.org/pypi/${PACKAGE}/${VERSION}/json" > /dev/null; then
|
|
161
|
+
echo "PyPI serves ${PACKAGE} ${VERSION}"
|
|
162
|
+
exit 0
|
|
163
|
+
fi
|
|
164
|
+
echo "attempt ${i}: PyPI doesn't serve ${PACKAGE} ${VERSION} yet"
|
|
165
|
+
sleep 15
|
|
166
|
+
done
|
|
167
|
+
echo "FAIL: PyPI never served ${PACKAGE} ${VERSION}"
|
|
168
|
+
exit 1
|
|
169
|
+
|
|
170
|
+
- name: Authenticate to MCP Registry (GitHub OIDC)
|
|
171
|
+
run: ./mcp-publisher login github-oidc
|
|
172
|
+
|
|
173
|
+
# Retries ride out the Registry's own transient failures. Before each
|
|
174
|
+
# retry, check whether an earlier attempt landed despite the error.
|
|
175
|
+
- name: Publish to MCP Registry
|
|
176
|
+
run: |
|
|
177
|
+
VERSION="${{ needs.security.outputs.version }}"
|
|
178
|
+
NAME="$(jq -r '.name' server.json)"
|
|
179
|
+
REGISTRY="https://registry.modelcontextprotocol.io/v0/servers?search=${NAME}&version=latest"
|
|
180
|
+
for i in 1 2 3 4 5; do
|
|
181
|
+
if ./mcp-publisher publish; then
|
|
182
|
+
exit 0
|
|
183
|
+
fi
|
|
184
|
+
echo "attempt ${i} failed; retrying in $((i * 30)) s"
|
|
185
|
+
sleep $((i * 30))
|
|
186
|
+
REG="$(curl -fsS "$REGISTRY" | jq -r --arg n "$NAME" '[.servers[] | select(.server.name == $n) | .server.version][0] // empty' || true)"
|
|
187
|
+
if [ "$REG" = "$VERSION" ]; then
|
|
188
|
+
echo "The Registry already has ${NAME} ${VERSION}; an earlier attempt landed"
|
|
189
|
+
exit 0
|
|
190
|
+
fi
|
|
191
|
+
done
|
|
192
|
+
echo "FAIL: the MCP Registry refused the publish 5 times"
|
|
193
|
+
exit 1
|
|
194
|
+
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
# Verify — a release is complete only when PyPI AND the MCP Registry both
|
|
197
|
+
# serve the tagged version. If either doesn't within 10 minutes, this run
|
|
198
|
+
# fails: a green run means published everywhere, a red one means not done.
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
verify:
|
|
201
|
+
name: Verify PyPI and MCP Registry
|
|
202
|
+
needs: [security, registry-publish]
|
|
203
|
+
runs-on: ubuntu-latest
|
|
204
|
+
steps:
|
|
205
|
+
- uses: actions/checkout@v5
|
|
206
|
+
|
|
207
|
+
- name: PyPI and Registry serve the released version
|
|
208
|
+
run: |
|
|
209
|
+
VERSION="${{ needs.security.outputs.version }}"
|
|
210
|
+
PACKAGE="$(jq -r '.packages[0].identifier' server.json)"
|
|
211
|
+
NAME="$(jq -r '.name' server.json)"
|
|
212
|
+
REGISTRY="https://registry.modelcontextprotocol.io/v0/servers?search=${NAME}&version=latest"
|
|
213
|
+
for i in $(seq 1 40); do
|
|
214
|
+
PYPI="$(curl -fsS "https://pypi.org/pypi/${PACKAGE}/json" | jq -r '.info.version' || true)"
|
|
215
|
+
REG="$(curl -fsS "$REGISTRY" | jq -r --arg n "$NAME" '[.servers[] | select(.server.name == $n) | .server.version][0] // empty' || true)"
|
|
216
|
+
echo "attempt ${i}: PyPI=${PYPI:-none} Registry=${REG:-none} want=${VERSION}"
|
|
217
|
+
if [ "$PYPI" = "$VERSION" ] && [ "$REG" = "$VERSION" ]; then
|
|
218
|
+
echo "Published everywhere: ${PACKAGE} ${VERSION} on PyPI and ${NAME} ${VERSION} in the MCP Registry"
|
|
219
|
+
exit 0
|
|
220
|
+
fi
|
|
221
|
+
sleep 15
|
|
222
|
+
done
|
|
223
|
+
echo "FAIL: release incomplete. PyPI=${PYPI:-none} Registry=${REG:-none}, expected ${VERSION}"
|
|
224
|
+
exit 1
|
|
225
|
+
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
# Tag the release, once it is published everywhere. The tag marks the commit on
|
|
228
|
+
# main that was released; nothing is triggered by it. If this job fails after a
|
|
229
|
+
# green verify, the release IS published: re-running this job is safe.
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
tag:
|
|
232
|
+
name: Tag the release
|
|
233
|
+
needs: [security, verify]
|
|
234
|
+
runs-on: ubuntu-latest
|
|
235
|
+
permissions:
|
|
236
|
+
contents: write
|
|
237
|
+
steps:
|
|
238
|
+
- uses: actions/checkout@v5
|
|
239
|
+
|
|
240
|
+
- name: Create and push vX.Y.Z
|
|
241
|
+
run: |
|
|
242
|
+
VERSION="${{ needs.security.outputs.version }}"
|
|
243
|
+
git config user.name "github-actions[bot]"
|
|
244
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
245
|
+
git tag -a "v${VERSION}" -m "${GITHUB_REPOSITORY#*/} ${VERSION}" "$GITHUB_SHA"
|
|
246
|
+
git push origin "v${VERSION}"
|
dxe_mcp-0.0.1/.gitignore
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `dxe-mcp` are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.0.1] — 2026-10-11
|
|
9
|
+
|
|
10
|
+
- Placeholder release so the name is held on PyPI while the work is planned
|
|
11
|
+
(qso-graph-devel#50). A pending trusted publisher does **not** reserve a name —
|
|
12
|
+
PyPI's documentation is explicit that a name is held only once something is actually
|
|
13
|
+
published — so this is a real, installable package rather than an empty repository.
|
|
14
|
+
- One tool, `get_version_info`, reporting the development status, what is planned, and
|
|
15
|
+
where the work is tracked. The server's instructions say the same, so a client listing
|
|
16
|
+
it sees the truth before calling anything.
|