Skip to content

Commit cafdd40

Browse files
feat(openapi): generated MCP/Python contract types + drift CI (#37 phase 2) (#48)
* feat(openapi): generate checked-in MCP/Python contract types (#37 phase 2) Implements phase 2 of #37 / closes #46. Generated artifacts are a contract reference; hand-written MCP/SDK public models are unchanged (no runtime or package behavior change — backward compatible). - MCP/TS: `openapi-typescript@7.4.4` (pinned devDep) -> checked-in packages/mcp/src/generated/openapi.d.ts; `npm run gen:openapi` regenerates. - Python SDK: `datamodel-code-generator==0.26.3` (pinned dev extra) -> checked-in packages/python-sdk/qveris/generated/openapi_models.py (pydantic v2, py3.8 target) + package __init__ documenting regeneration. - CI: .github/workflows/openapi-types.yml regenerates both with the pinned generators and fails on `git diff --exit-code`, path-scoped to docs/openapi/** + the generators (mirrors openapi-contract.yml from #44). Verified: artifacts are byte-reproducible from the checked-in spec (empty diff on regen), the .d.ts typechecks under tsc --strict, and the Python models pass py_compile. * feat(contract): CLI + Python OpenAPI contract guards (#37 phase 3) (#49) * feat(contract): adopt generated OpenAPI types — CLI + Python guards (#37 phase 3) Implements phase 3 of #37 / closes #47. First low-risk adoption step; public MCP/CLI/Python SDK APIs unchanged (additive tests + CI only). - CLI (JS/MJS): packages/cli/test/openapi-contract.test.mjs asserts every endpoint+method the CLI calls (kept in sync with src/client/api.mjs: POST /search, /tools/by-ids, /tools/execute; GET /auth/credits, /auth/usage/history/v2, /auth/credits/ledger) exists in the mirrored public OpenAPI spec — the issue's "CLI contract tests before type gen". - Python SDK: tests/test_openapi_contract.py starts *consuming* the generated qveris.generated.openapi_models as a drift guard (core contract models present + pydantic + smoke roundtrip). Hand-written qveris.types stays the public surface; field alignment stays gradual by design. - CI: .github/workflows/contract-tests.yml runs both on PRs (the existing cli/python *-publish workflows only fire on release tags, so without this the guards would not gate PRs). Verified locally: CLI `node --test` 7/7 pass; Python pytest 11/11 pass. Builds on #46/#48 (needs the generated artifacts) — merge after #48. * fix(contract): load generated models standalone + robust shape check (#47) CI python-contract failed: importing qveris.generated.openapi_models pulled qveris/__init__.py (httpx not installed in the minimal contract job). Load the generated file by path so the guard only needs pydantic. Replace the instantiation smoke with a model_fields shape assertion (the generated file's __future__ annotations + constrained types need model_rebuild() to validate; the field map is the stable contract signal regardless). * Update packages/mcp/package.json Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 116775d commit cafdd40

9 files changed

Lines changed: 2944 additions & 5 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: Contract tests
2+
3+
# Issue #37 phase 3 (#47): enforce, on every PR, that the CLI's API calls and
4+
# the Python SDK's consumed models stay aligned with the website-mirrored
5+
# public OpenAPI contract. The cli/python *-publish workflows only run on
6+
# release tags, so without this the contract guards would not gate PRs.
7+
8+
on:
9+
pull_request:
10+
paths:
11+
- "docs/openapi/**"
12+
- "packages/cli/src/**"
13+
- "packages/cli/test/openapi-contract.test.mjs"
14+
- "packages/python-sdk/qveris/**"
15+
- "packages/python-sdk/tests/test_openapi_contract.py"
16+
- ".github/workflows/contract-tests.yml"
17+
push:
18+
branches:
19+
- main
20+
paths:
21+
- "docs/openapi/**"
22+
- "packages/cli/src/**"
23+
- "packages/cli/test/openapi-contract.test.mjs"
24+
- "packages/python-sdk/qveris/**"
25+
- "packages/python-sdk/tests/test_openapi_contract.py"
26+
- ".github/workflows/contract-tests.yml"
27+
28+
permissions:
29+
contents: read
30+
31+
jobs:
32+
cli-contract:
33+
runs-on: ubuntu-latest
34+
steps:
35+
- uses: actions/checkout@v4
36+
- uses: actions/setup-node@v4
37+
with:
38+
node-version: 20
39+
- name: CLI ↔ OpenAPI contract test
40+
working-directory: packages/cli
41+
run: node --test test/openapi-contract.test.mjs
42+
43+
python-contract:
44+
runs-on: ubuntu-latest
45+
steps:
46+
- uses: actions/checkout@v4
47+
- uses: actions/setup-python@v5
48+
with:
49+
python-version: "3.11"
50+
- name: Python SDK ↔ generated-contract test
51+
working-directory: packages/python-sdk
52+
run: |
53+
python -m pip install --quiet pytest "pydantic>=2.0.0"
54+
PYTHONPATH=. python -m pytest tests/test_openapi_contract.py -q
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: OpenAPI generated types
2+
3+
# Issue #37 phase 2: the checked-in generated artifacts must be reproducible
4+
# from docs/openapi/qveris-public-api.openapi.json. Regenerate with the pinned
5+
# generators and fail if the working tree differs (contract / generator drift).
6+
7+
on:
8+
pull_request:
9+
paths:
10+
- "docs/openapi/**"
11+
- "packages/mcp/src/generated/**"
12+
- "packages/python-sdk/qveris/generated/**"
13+
- "packages/mcp/package.json"
14+
- "packages/python-sdk/pyproject.toml"
15+
- ".github/workflows/openapi-types.yml"
16+
push:
17+
branches:
18+
- main
19+
paths:
20+
- "docs/openapi/**"
21+
- "packages/mcp/src/generated/**"
22+
- "packages/python-sdk/qveris/generated/**"
23+
- "packages/mcp/package.json"
24+
- "packages/python-sdk/pyproject.toml"
25+
- ".github/workflows/openapi-types.yml"
26+
27+
permissions:
28+
contents: read
29+
30+
jobs:
31+
regen-and-diff:
32+
runs-on: ubuntu-latest
33+
steps:
34+
- name: Checkout
35+
uses: actions/checkout@v4
36+
37+
- name: Setup Node.js
38+
uses: actions/setup-node@v4
39+
with:
40+
node-version: 20
41+
42+
- name: Setup Python
43+
uses: actions/setup-python@v5
44+
with:
45+
python-version: "3.11"
46+
47+
- name: Regenerate MCP TypeScript types
48+
run: npx --yes openapi-typescript@7.4.4 docs/openapi/qveris-public-api.openapi.json -o packages/mcp/src/generated/openapi.d.ts
49+
50+
- name: Regenerate Python SDK models
51+
run: |
52+
python -m pip install --quiet "datamodel-code-generator==0.26.3"
53+
python -m datamodel_code_generator \
54+
--input docs/openapi/qveris-public-api.openapi.json \
55+
--input-file-type openapi \
56+
--output packages/python-sdk/qveris/generated/openapi_models.py \
57+
--output-model-type pydantic_v2.BaseModel \
58+
--target-python-version 3.8 \
59+
--use-schema-description \
60+
--disable-timestamp
61+
62+
- name: Fail on drift
63+
run: |
64+
if ! git diff --exit-code -- \
65+
packages/mcp/src/generated/openapi.d.ts \
66+
packages/python-sdk/qveris/generated/openapi_models.py; then
67+
echo "::error::Generated OpenAPI artifacts are out of date. Run 'npm --prefix packages/mcp run gen:openapi' and regenerate the Python models, then commit."
68+
exit 1
69+
fi
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Issue #37 phase 3 (#47): CLI is JavaScript/MJS, so before any type
2+
// generation it gets contract tests that assert every endpoint + method the
3+
// CLI actually calls exists in the website-mirrored public OpenAPI spec.
4+
// This catches CLI/contract drift without introducing a generator.
5+
6+
import assert from "node:assert/strict";
7+
import { readFileSync } from "node:fs";
8+
import { fileURLToPath } from "node:url";
9+
import path from "node:path";
10+
import test from "node:test";
11+
12+
const here = path.dirname(fileURLToPath(import.meta.url));
13+
const specPath = path.resolve(
14+
here,
15+
"../../../docs/openapi/qveris-public-api.openapi.json",
16+
);
17+
18+
const spec = JSON.parse(readFileSync(specPath, "utf8"));
19+
20+
// Endpoints the CLI calls today, kept in sync with packages/cli/src/client/api.mjs.
21+
// (path, method) — method lowercased to match OpenAPI operation keys.
22+
const CLI_OPERATIONS = [
23+
["/search", "post"],
24+
["/tools/by-ids", "post"],
25+
["/tools/execute", "post"],
26+
["/auth/credits", "get"],
27+
["/auth/usage/history/v2", "get"],
28+
["/auth/credits/ledger", "get"],
29+
];
30+
31+
test("OpenAPI spec is structurally usable", () => {
32+
assert.equal(typeof spec, "object");
33+
assert.ok(spec.info && typeof spec.info.version === "string", "info.version present");
34+
assert.ok(spec.paths && typeof spec.paths === "object", "paths present");
35+
});
36+
37+
for (const [p, method] of CLI_OPERATIONS) {
38+
test(`contract covers CLI call ${method.toUpperCase()} ${p}`, () => {
39+
const item = spec.paths[p];
40+
assert.ok(item, `path ${p} missing from public OpenAPI contract`);
41+
assert.ok(
42+
item[method],
43+
`method ${method.toUpperCase()} for ${p} missing — CLI calls it but the contract does not declare it`,
44+
);
45+
});
46+
}

packages/mcp/package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@qverisai/mcp",
3-
"version": "0.6.0",
4-
"description": "QVeris MCP server for agent tool discovery, inspection, and calling",
3+
"version": "0.6.0",
4+
"description": "QVeris MCP server for agent tool discovery, inspection, and calling",
55
"keywords": [
66
"qveris",
77
"mcp",
@@ -14,12 +14,12 @@
1414
"license": "MIT",
1515
"repository": {
1616
"type": "git",
17-
"url": "https://github.com/QVerisAI/qveris-agent-toolkit",
17+
"url": "https://github.com/QVerisAI/qveris-agent-toolkit",
1818
"directory": "packages/mcp"
1919
},
2020
"homepage": "https://qveris.ai",
2121
"bugs": {
22-
"url": "https://github.com/QVerisAI/qveris-agent-toolkit/issues"
22+
"url": "https://github.com/QVerisAI/qveris-agent-toolkit/issues"
2323
},
2424
"type": "module",
2525
"main": "dist/index.js",
@@ -38,7 +38,8 @@
3838
"test:watch": "vitest",
3939
"test:coverage": "vitest run --coverage",
4040
"prepublishOnly": "npm run build",
41-
"typecheck": "tsc --noEmit"
41+
"typecheck": "tsc --noEmit",
42+
"gen:openapi": "openapi-typescript ../../docs/openapi/qveris-public-api.openapi.json -o src/generated/openapi.d.ts && npx prettier --write src/generated/openapi.d.ts"
4243
},
4344
"dependencies": {
4445
"@modelcontextprotocol/sdk": "^1.0.0",
@@ -47,6 +48,7 @@
4748
"devDependencies": {
4849
"@types/node": "^22.10.1",
4950
"@types/uuid": "^10.0.0",
51+
"openapi-typescript": "7.4.4",
5052
"typescript": "^5.7.2",
5153
"vitest": "^2.1.0"
5254
},

0 commit comments

Comments
 (0)