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.
- qsp_client-0.3.0/.github/workflows/ci.yml +55 -0
- qsp_client-0.3.0/.github/workflows/publish-shim.yml +67 -0
- qsp_client-0.3.0/.github/workflows/publish.yml +190 -0
- qsp_client-0.3.0/.gitignore +9 -0
- qsp_client-0.3.0/CHANGELOG.md +36 -0
- qsp_client-0.3.0/LICENSE +674 -0
- qsp_client-0.3.0/PKG-INFO +168 -0
- qsp_client-0.3.0/README.md +140 -0
- qsp_client-0.3.0/pyproject.toml +49 -0
- qsp_client-0.3.0/server.json +21 -0
- qsp_client-0.3.0/shim/README.md +13 -0
- qsp_client-0.3.0/shim/pyproject.toml +31 -0
- qsp_client-0.3.0/src/qsp_client/__init__.py +10 -0
- qsp_client-0.3.0/src/qsp_client/cli.py +243 -0
- qsp_client-0.3.0/src/qsp_client/client.py +130 -0
- qsp_client-0.3.0/src/qsp_client/config.py +147 -0
- qsp_client-0.3.0/src/qsp_client/relay.py +365 -0
- qsp_client-0.3.0/src/qsp_client/schema.py +180 -0
- qsp_client-0.3.0/tests/test_rename.py +56 -0
- qsp_client-0.3.0/tests/test_security.py +81 -0
|
@@ -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,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.
|