qsp-client 0.3.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.
@@ -0,0 +1,55 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main]
6
+ push:
7
+ branches: [main]
8
+
9
+ # Cancel superseded runs on the same ref (push or PR) so we don't waste
10
+ # minutes when commits land in rapid succession.
11
+ concurrency:
12
+ group: ci-${{ github.workflow }}-${{ github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ test:
17
+ name: pytest (${{ matrix.python-version }})
18
+ runs-on: ubuntu-latest
19
+ strategy:
20
+ fail-fast: false
21
+ matrix:
22
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+
26
+ - name: Set up Python ${{ matrix.python-version }}
27
+ uses: actions/setup-python@v5
28
+ with:
29
+ python-version: ${{ matrix.python-version }}
30
+
31
+ - name: Install package + test dependencies
32
+ run: |
33
+ python -m pip install --upgrade pip
34
+ pip install -e .
35
+ pip install pytest pytest-asyncio
36
+
37
+ - name: Run unit tests (L2)
38
+ run: python -m pytest tests --ignore=tests/test_security.py -v
39
+
40
+ - name: Run security tests
41
+ run: python -m pytest tests/test_security.py -v
42
+
43
+ ci-all-green:
44
+ name: ci-all-green
45
+ needs: [test]
46
+ runs-on: ubuntu-latest
47
+ if: always()
48
+ steps:
49
+ - name: Verify required jobs all succeeded
50
+ run: |
51
+ if [[ "${{ needs.test.result }}" != "success" ]]; then
52
+ echo "FAIL: test matrix did not all succeed (result=${{ needs.test.result }})"
53
+ exit 1
54
+ fi
55
+ echo "All required CI jobs passed."
@@ -0,0 +1,67 @@
1
+ name: Publish the qsp-mcp rename shim
2
+
3
+ # Run once, by hand, after qsp-client 0.3.0 is published everywhere:
4
+ # 1. publish qsp-mcp 0.3.0 to PyPI: no code, it installs qsp-client
5
+ # 2. mark the old io.github.qso-graph/qsp-mcp Registry entry deprecated,
6
+ # pointing to io.github.qso-graph/qsp-client
7
+ # Needs qsp-mcp's PyPI trusted publisher set to this repo and this workflow.
8
+
9
+ on:
10
+ workflow_dispatch:
11
+
12
+ jobs:
13
+ shim:
14
+ name: Publish qsp-mcp 0.3.0 (installs qsp-client)
15
+ runs-on: ubuntu-latest
16
+ environment: pypi
17
+ permissions:
18
+ id-token: write
19
+ steps:
20
+ - uses: actions/checkout@v5
21
+
22
+ - uses: actions/setup-python@v5
23
+ with:
24
+ python-version: "3.12"
25
+
26
+ - name: qsp-client must already be on PyPI
27
+ run: |
28
+ WANT="$(grep -m1 '"qsp-client>=' shim/pyproject.toml | sed -E 's/.*>=([0-9.]+).*/\1/')"
29
+ HAVE="$(curl -fsS https://pypi.org/pypi/qsp-client/json | jq -r '.info.version')"
30
+ echo "shim needs qsp-client>=${WANT}; PyPI has ${HAVE}"
31
+ python3 -c "import sys; v=lambda s: tuple(map(int, s.split('.'))); sys.exit(0 if v('${HAVE}') >= v('${WANT}') else 1)"
32
+
33
+ - name: Build the shim
34
+ run: |
35
+ pip install build
36
+ python -m build shim --outdir dist
37
+
38
+ - name: Publish to PyPI
39
+ uses: pypa/gh-action-pypi-publish@release/v1
40
+
41
+ deprecate:
42
+ name: Deprecate the old Registry entry
43
+ needs: shim
44
+ runs-on: ubuntu-latest
45
+ permissions:
46
+ id-token: write
47
+ contents: read
48
+ steps:
49
+ - name: Install mcp-publisher
50
+ run: |
51
+ 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
52
+ ./mcp-publisher --help > /dev/null
53
+
54
+ - name: The new entry must already be in the Registry
55
+ run: |
56
+ REG="$(curl -fsS 'https://registry.modelcontextprotocol.io/v0/servers?search=io.github.qso-graph/qsp-client&version=latest' | jq -r '[.servers[] | select(.server.name == "io.github.qso-graph/qsp-client")][0].server.version // empty')"
57
+ echo "io.github.qso-graph/qsp-client in the Registry: ${REG:-none}"
58
+ [ -n "$REG" ]
59
+
60
+ - name: Authenticate to MCP Registry (GitHub OIDC)
61
+ run: ./mcp-publisher login github-oidc
62
+
63
+ - name: Deprecate io.github.qso-graph/qsp-mcp
64
+ run: |
65
+ ./mcp-publisher status --status deprecated --all-versions \
66
+ --message "Renamed to io.github.qso-graph/qsp-client (pip install qsp-client)" \
67
+ io.github.qso-graph/qsp-mcp
@@ -0,0 +1,190 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ security:
10
+ name: Security gates
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ # PyPI publishes pyproject's version; the Registry publishes the tag's.
16
+ # They must be the same, or the two would disagree about what was released.
17
+ - name: Tag matches pyproject.toml version
18
+ run: |
19
+ TAG_VERSION="${GITHUB_REF_NAME#v}"
20
+ PKG_VERSION="$(grep -m1 '^version' pyproject.toml | cut -d'"' -f2)"
21
+ echo "tag=${TAG_VERSION} pyproject=${PKG_VERSION}"
22
+ if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
23
+ echo "FAIL: tag v${TAG_VERSION} does not match pyproject.toml version ${PKG_VERSION}"
24
+ exit 1
25
+ fi
26
+
27
+
28
+ - uses: actions/setup-python@v5
29
+ with:
30
+ python-version: "3.12"
31
+
32
+ - name: Install test dependencies
33
+ run: pip install pytest pip-audit
34
+
35
+ - name: Run security test suite
36
+ run: python -m pytest tests/test_security.py -v
37
+
38
+ - name: Audit dependencies for vulnerabilities
39
+ run: pip-audit --strict --desc 2>&1 || true
40
+
41
+ - name: Grep check — no subprocess/shell
42
+ run: |
43
+ if grep -rn 'subprocess\.\|os\.system\|shell=True' src/; then
44
+ echo "FAIL: subprocess/shell found in source"
45
+ exit 1
46
+ fi
47
+
48
+ - name: Grep check — no plaintext http://
49
+ run: |
50
+ if grep -rn 'http://' src/ | grep -v 'localhost\|127\.0\.0\.1\|::1\|10\.\|192\.168\.'; then
51
+ echo "FAIL: non-HTTPS URL found in source"
52
+ exit 1
53
+ fi
54
+
55
+ - name: Grep check — no credential logging
56
+ run: |
57
+ if grep -rn -i 'print.*password\|print.*api_key\|logging.*password\|logging.*api_key' src/; then
58
+ echo "FAIL: credential logging found in source"
59
+ exit 1
60
+ fi
61
+
62
+ publish:
63
+ name: Build and publish to PyPI
64
+ needs: security
65
+ runs-on: ubuntu-latest
66
+ environment: pypi
67
+ permissions:
68
+ id-token: write # Required for trusted publishing (OIDC)
69
+ steps:
70
+ - uses: actions/checkout@v4
71
+
72
+ - uses: actions/setup-python@v5
73
+ with:
74
+ python-version: "3.12"
75
+
76
+ - name: Install build dependencies
77
+ run: pip install build
78
+
79
+ - name: Build package
80
+ run: python -m build
81
+
82
+ - name: Publish to PyPI
83
+ uses: pypa/gh-action-pypi-publish@release/v1
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Registry-publish — pushes server.json to the Official MCP Registry after
87
+ # PyPI publish succeeds, so discovery surfaces stay in sync with PyPI.
88
+ # Canonical template: https://github.com/qso-graph/.github/blob/main/TEMPLATES.md
89
+ # ---------------------------------------------------------------------------
90
+ registry-publish:
91
+ name: Publish to MCP Registry
92
+ needs: publish # waits for PyPI publish to succeed
93
+ runs-on: ubuntu-latest
94
+ permissions:
95
+ id-token: write # GitHub OIDC
96
+ contents: read
97
+
98
+ steps:
99
+ - uses: actions/checkout@v5
100
+
101
+ - name: Install jq (server.json bump)
102
+ run: sudo apt-get update && sudo apt-get install -y jq
103
+
104
+ - name: Install mcp-publisher
105
+ run: |
106
+ 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
107
+ ./mcp-publisher --help > /dev/null # smoke test
108
+
109
+ - name: Bump server.json to tag version
110
+ run: |
111
+ TAG_VERSION="${GITHUB_REF_NAME#v}"
112
+ echo "Setting server.json to ${TAG_VERSION}"
113
+ jq --arg v "$TAG_VERSION" \
114
+ '.version = $v | .packages[0].version = $v' \
115
+ server.json > /tmp/server.json
116
+ mv /tmp/server.json server.json
117
+ cat server.json
118
+
119
+ # The Registry checks PyPI itself and refuses a version PyPI isn't
120
+ # serving yet, which can lag the upload by a minute or more.
121
+ - name: Wait until PyPI serves this version
122
+ run: |
123
+ TAG_VERSION="${GITHUB_REF_NAME#v}"
124
+ PACKAGE="$(jq -r '.packages[0].identifier' server.json)"
125
+ for i in $(seq 1 40); do
126
+ if curl -fsS "https://pypi.org/pypi/${PACKAGE}/${TAG_VERSION}/json" > /dev/null; then
127
+ echo "PyPI serves ${PACKAGE} ${TAG_VERSION}"
128
+ exit 0
129
+ fi
130
+ echo "attempt ${i}: PyPI doesn't serve ${PACKAGE} ${TAG_VERSION} yet"
131
+ sleep 15
132
+ done
133
+ echo "FAIL: PyPI never served ${PACKAGE} ${TAG_VERSION}"
134
+ exit 1
135
+
136
+ - name: Authenticate to MCP Registry (GitHub OIDC)
137
+ run: ./mcp-publisher login github-oidc
138
+
139
+ # Retries ride out the Registry's own transient failures. Before each
140
+ # retry, check whether an earlier attempt landed despite the error.
141
+ - name: Publish to MCP Registry
142
+ run: |
143
+ TAG_VERSION="${GITHUB_REF_NAME#v}"
144
+ NAME="$(jq -r '.name' server.json)"
145
+ REGISTRY="https://registry.modelcontextprotocol.io/v0/servers?search=${NAME}&version=latest"
146
+ for i in 1 2 3 4 5; do
147
+ if ./mcp-publisher publish; then
148
+ exit 0
149
+ fi
150
+ echo "attempt ${i} failed; retrying in $((i * 30)) s"
151
+ sleep $((i * 30))
152
+ REG="$(curl -fsS "$REGISTRY" | jq -r --arg n "$NAME" '[.servers[] | select(.server.name == $n) | .server.version][0] // empty' || true)"
153
+ if [ "$REG" = "$TAG_VERSION" ]; then
154
+ echo "The Registry already has ${NAME} ${TAG_VERSION}; an earlier attempt landed"
155
+ exit 0
156
+ fi
157
+ done
158
+ echo "FAIL: the MCP Registry refused the publish 5 times"
159
+ exit 1
160
+
161
+ # ---------------------------------------------------------------------------
162
+ # Verify — a release is complete only when PyPI AND the MCP Registry both
163
+ # serve the tagged version. If either doesn't within 10 minutes, this run
164
+ # fails: a green run means published everywhere, a red one means not done.
165
+ # ---------------------------------------------------------------------------
166
+ verify:
167
+ name: Verify PyPI and MCP Registry
168
+ needs: registry-publish
169
+ runs-on: ubuntu-latest
170
+ steps:
171
+ - uses: actions/checkout@v5
172
+
173
+ - name: PyPI and Registry serve the tagged version
174
+ run: |
175
+ TAG_VERSION="${GITHUB_REF_NAME#v}"
176
+ PACKAGE="$(jq -r '.packages[0].identifier' server.json)"
177
+ NAME="$(jq -r '.name' server.json)"
178
+ REGISTRY="https://registry.modelcontextprotocol.io/v0/servers?search=${NAME}&version=latest"
179
+ for i in $(seq 1 40); do
180
+ PYPI="$(curl -fsS "https://pypi.org/pypi/${PACKAGE}/json" | jq -r '.info.version' || true)"
181
+ REG="$(curl -fsS "$REGISTRY" | jq -r --arg n "$NAME" '[.servers[] | select(.server.name == $n) | .server.version][0] // empty' || true)"
182
+ echo "attempt ${i}: PyPI=${PYPI:-none} Registry=${REG:-none} want=${TAG_VERSION}"
183
+ if [ "$PYPI" = "$TAG_VERSION" ] && [ "$REG" = "$TAG_VERSION" ]; then
184
+ echo "Published everywhere: ${PACKAGE} ${TAG_VERSION} on PyPI and ${NAME} ${TAG_VERSION} in the MCP Registry"
185
+ exit 0
186
+ fi
187
+ sleep 15
188
+ done
189
+ echo "FAIL: release incomplete. PyPI=${PYPI:-none} Registry=${REG:-none}, expected ${TAG_VERSION}"
190
+ exit 1
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ uv.lock
8
+ .pytest_cache/
9
+ .mcpregistry_*
@@ -0,0 +1,36 @@
1
+ # Changelog
2
+
3
+ All notable changes to `qsp-client` (formerly `qsp-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.3.0] — 2026-09-28
9
+
10
+ ### Changed
11
+
12
+ - **Renamed from qsp-mcp to qsp-client.** In the MCP world, `-mcp` names mark servers, and this is a
13
+ client. The PyPI package, the Python module (`qsp_client`), the command, the repository and the MCP
14
+ Registry entry (`io.github.qso-graph/qsp-client`) all use the new name.
15
+ - Config is read from `~/.config/qsp-client/config.json` or `~/.qsp-client.json`, then from the old
16
+ `~/.config/qsp-mcp/config.json` or `~/.qsp-mcp.json`, so existing setups keep working.
17
+ - The `qsp-mcp` command still works, and says it has moved.
18
+ - A final `qsp-mcp` 0.3.0 on PyPI has no code and installs qsp-client, so `pip install -U qsp-mcp`
19
+ moves users over. The old Registry entry is marked deprecated, pointing to the new one.
20
+
21
+ ### Added (CI hygiene)
22
+
23
+ - **MCP Registry sync** — `publish.yml` publishes to the [Official MCP Registry](https://registry.modelcontextprotocol.io)
24
+ after each PyPI publish, using GitHub OIDC for auth. Triggered on
25
+ `v*` tag push; no manual steps. The Registry job waits until PyPI
26
+ serves the version, and retries. Pattern documented in
27
+ [qso-graph/.github/TEMPLATES.md](https://github.com/qso-graph/.github/blob/main/TEMPLATES.md).
28
+ - **Registry version badge** in README.
29
+ - **Release gates** — the tag must match `pyproject.toml`, and a
30
+ `verify` job fails the release unless PyPI and the MCP Registry
31
+ both serve the new version.
32
+ - **CI** on pull requests (Python 3.10–3.13).
33
+
34
+ ## [0.2.2] and earlier
35
+
36
+ Released as qsp-mcp; see the git history.