From f1daa641409a81528d8df06b58a6c1360a2dba68 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Tue, 25 Aug 2026 05:09:59 +0200 Subject: [PATCH 1/6] feat(mcp): add server.json for the MCP Registry + PR/tag validation CI Adds server.json at the repo root under the io.github.leanercloud namespace (permanent once published -- GitHub-OIDC-verified, casing matches the registry's io.github. examples). The registry has no raw-Go-binary package type, so the single packages[] entry is registryType "mcpb", pointing at the (not-yet-existing) GitHub Release MCPB asset the follow-up release.yml PR will produce; fileSha256 is a placeholder 64-zero hash until that PR wires up patching it from the actual built artifact at publish time. environmentVariables documents both CUDLY_MCP_ENABLE_REAL_PURCHASES and CUDLY_MCP_AUDIT_LOG openly, per the registry's financial-transactions disclosure requirement. .github/workflows/mcp-server-json.yml validates server.json against the registry's published JSON Schema on every PR that touches it, and on v* tag pushes asserts server.json's version matches the tag -- a mismatch fails the workflow loudly rather than letting a later publish step silently ship the wrong metadata (the registry rejects republishing a version anyway, so this catches the mistake before that point). --- .github/workflows/mcp-server-json.yml | 68 +++++++++++++++++++++++++++ server.json | 37 +++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 .github/workflows/mcp-server-json.yml create mode 100644 server.json diff --git a/.github/workflows/mcp-server-json.yml b/.github/workflows/mcp-server-json.yml new file mode 100644 index 000000000..b15220cf5 --- /dev/null +++ b/.github/workflows/mcp-server-json.yml @@ -0,0 +1,68 @@ +name: MCP server.json + +# Validates server.json (the MCP Registry listing manifest, see +# docs/plans/mcp/05-store.md Phase B) on every PR that touches it, and gates +# tag pushes on server.json's version matching the tag -- the registry +# rejects republishing a version, so a mismatch here must fail loud rather +# than let release.yml silently publish the wrong metadata. + +on: + pull_request: + paths: + - "server.json" + - ".github/workflows/mcp-server-json.yml" + push: + tags: ["v*"] + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate-schema: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + persist-credentials: false + + - name: Fetch the MCP Registry server.json schema + run: | + set -euo pipefail + schema_url=$(jq -r '.["$schema"]' server.json) + if [[ -z "$schema_url" || "$schema_url" == "null" ]]; then + echo "::error::server.json is missing a \$schema field" + exit 1 + fi + curl -sSfL "$schema_url" -o /tmp/server.schema.json + + - name: Validate server.json against the schema + run: npx --yes ajv-cli@5 validate -s /tmp/server.schema.json -d server.json --spec=draft7 --strict=false + + assert-version-matches-tag: + if: startsWith(github.ref, 'refs/tags/v') + needs: validate-schema + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + persist-credentials: false + + - name: Assert server.json version matches the pushed tag + run: | + set -euo pipefail + tag="${GITHUB_REF#refs/tags/v}" + server_version=$(jq -r '.version' server.json) + if [[ "$tag" != "$server_version" ]]; then + echo "::error::server.json version ($server_version) does not match tag v$tag." \ + "Bump server.json's version (and, for the MCPB package entry," \ + "packages[].identifier and packages[].fileSha256) in the release PR before tagging -- this never rewrites the file for you." + exit 1 + fi + echo "server.json version ($server_version) matches tag v$tag" diff --git a/server.json b/server.json new file mode 100644 index 000000000..dac421f7e --- /dev/null +++ b/server.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.leanercloud/cudly-mcp", + "description": "Search and buy AWS/Azure/GCP reserved capacity (RIs, Savings Plans, CUDs) via any MCP client.", + "repository": { + "url": "https://github.com/LeanerCloud/CUDly", + "source": "github" + }, + "version": "0.1.0", + "websiteUrl": "https://cudly.io/mcp/", + "packages": [ + { + "registryType": "mcpb", + "identifier": "https://github.com/LeanerCloud/CUDly/releases/download/v0.1.0/cudly-mcp-full.mcpb", + "fileSha256": "0000000000000000000000000000000000000000000000000000000000000000", + "transport": { + "type": "stdio" + }, + "environmentVariables": [ + { + "name": "CUDLY_MCP_ENABLE_REAL_PURCHASES", + "description": "Set to 1 (or true) to let purchase tools execute real, money-spending purchases. Unset (the default) refuses every dry_run=false call before any provider or credential is touched -- see mcp/README.md 'Safety model'.", + "isRequired": false, + "isSecret": false, + "format": "string" + }, + { + "name": "CUDLY_MCP_AUDIT_LOG", + "description": "Overrides the JSONL audit log path for purchase attempts; set to an empty string to disable audit logging. See mcp/README.md for the current default path.", + "isRequired": false, + "isSecret": false, + "format": "filepath" + } + ] + } + ] +} From 43afdb1beff70b5aaa5467594ada755a38f0b2ee Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Sat, 29 Aug 2026 01:22:50 +0200 Subject: [PATCH 2/6] ci(mcp): pin registry schema validation URL Prevent pull request content from selecting the schema used to validate server.json. Require the manifest declaration to exactly match the pinned official Registry schema before fetching it. --- .github/workflows/mcp-server-json.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/mcp-server-json.yml b/.github/workflows/mcp-server-json.yml index b15220cf5..257e5bac5 100644 --- a/.github/workflows/mcp-server-json.yml +++ b/.github/workflows/mcp-server-json.yml @@ -32,12 +32,13 @@ jobs: - name: Fetch the MCP Registry server.json schema run: | set -euo pipefail - schema_url=$(jq -r '.["$schema"]' server.json) - if [[ -z "$schema_url" || "$schema_url" == "null" ]]; then - echo "::error::server.json is missing a \$schema field" + expected_schema_url="https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json" + if ! jq -e --arg expected "$expected_schema_url" \ + '.["$schema"] | type == "string" and . == $expected' server.json >/dev/null; then + echo "::error::server.json must declare the official MCP Registry \$schema URL" exit 1 fi - curl -sSfL "$schema_url" -o /tmp/server.schema.json + curl -sSfL "$expected_schema_url" -o /tmp/server.schema.json - name: Validate server.json against the schema run: npx --yes ajv-cli@5 validate -s /tmp/server.schema.json -d server.json --spec=draft7 --strict=false From 97a467ddc98a90b5acac3c49396df7980b41c161 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Mon, 31 Aug 2026 12:30:07 +0200 Subject: [PATCH 3/6] ci(mcp): enforce registry URI formats --- .../server-json-validator/package-lock.json | 84 +++++++++++++++++++ .../server-json-validator/package.json | 8 ++ .../server-json-validator/validate.cjs | 22 +++++ .github/workflows/mcp-server-json.yml | 7 +- 4 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 .github/scripts/server-json-validator/package-lock.json create mode 100644 .github/scripts/server-json-validator/package.json create mode 100644 .github/scripts/server-json-validator/validate.cjs diff --git a/.github/scripts/server-json-validator/package-lock.json b/.github/scripts/server-json-validator/package-lock.json new file mode 100644 index 000000000..3b345d852 --- /dev/null +++ b/.github/scripts/server-json-validator/package-lock.json @@ -0,0 +1,84 @@ +{ + "name": "cudly-mcp-server-json-validator", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cudly-mcp-server-json-validator", + "dependencies": { + "ajv": "8.20.0", + "ajv-formats": "3.0.1" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz", + "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + } + } +} diff --git a/.github/scripts/server-json-validator/package.json b/.github/scripts/server-json-validator/package.json new file mode 100644 index 000000000..5e759c0fa --- /dev/null +++ b/.github/scripts/server-json-validator/package.json @@ -0,0 +1,8 @@ +{ + "name": "cudly-mcp-server-json-validator", + "private": true, + "dependencies": { + "ajv": "8.20.0", + "ajv-formats": "3.0.1" + } +} diff --git a/.github/scripts/server-json-validator/validate.cjs b/.github/scripts/server-json-validator/validate.cjs new file mode 100644 index 000000000..3a182e6ce --- /dev/null +++ b/.github/scripts/server-json-validator/validate.cjs @@ -0,0 +1,22 @@ +const fs = require("fs"); +const Ajv = require("ajv"); +const addFormats = require("ajv-formats"); + +const [schemaPath, dataPath] = process.argv.slice(2); +if (!schemaPath || !dataPath) { + console.error("usage: node .github/scripts/server-json-validator/validate.cjs "); + process.exit(2); +} + +const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8")); +const data = JSON.parse(fs.readFileSync(dataPath, "utf8")); +const ajv = new Ajv({ strict: false, allErrors: true }); +addFormats(ajv); + +const valid = ajv.validate(schema, data); +if (!valid) { + console.error(JSON.stringify(ajv.errors, null, 2)); + process.exit(1); +} + +console.log(`${dataPath} valid`); diff --git a/.github/workflows/mcp-server-json.yml b/.github/workflows/mcp-server-json.yml index 257e5bac5..a1bdc4625 100644 --- a/.github/workflows/mcp-server-json.yml +++ b/.github/workflows/mcp-server-json.yml @@ -11,6 +11,7 @@ on: paths: - "server.json" - ".github/workflows/mcp-server-json.yml" + - ".github/scripts/server-json-validator/**" push: tags: ["v*"] workflow_dispatch: @@ -40,8 +41,12 @@ jobs: fi curl -sSfL "$expected_schema_url" -o /tmp/server.schema.json + - name: Install server.json validator dependencies + working-directory: .github/scripts/server-json-validator + run: npm ci --ignore-scripts --no-audit --no-fund + - name: Validate server.json against the schema - run: npx --yes ajv-cli@5 validate -s /tmp/server.schema.json -d server.json --spec=draft7 --strict=false + run: node .github/scripts/server-json-validator/validate.cjs /tmp/server.schema.json server.json assert-version-matches-tag: if: startsWith(github.ref, 'refs/tags/v') From c70e616468a6e7961a59c5900085ddc314029954 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Mon, 31 Aug 2026 12:38:52 +0200 Subject: [PATCH 4/6] fix(mcp): use canonical registry namespace --- .github/workflows/mcp-server-json.yml | 6 ++++++ server.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/mcp-server-json.yml b/.github/workflows/mcp-server-json.yml index a1bdc4625..6979e9f93 100644 --- a/.github/workflows/mcp-server-json.yml +++ b/.github/workflows/mcp-server-json.yml @@ -34,11 +34,17 @@ jobs: run: | set -euo pipefail expected_schema_url="https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json" + expected_registry_name="io.github.LeanerCloud/cudly-mcp" if ! jq -e --arg expected "$expected_schema_url" \ '.["$schema"] | type == "string" and . == $expected' server.json >/dev/null; then echo "::error::server.json must declare the official MCP Registry \$schema URL" exit 1 fi + if ! jq -e --arg expected "$expected_registry_name" \ + '.name | type == "string" and . == $expected' server.json >/dev/null; then + echo "::error::server.json name must be $expected_registry_name" + exit 1 + fi curl -sSfL "$expected_schema_url" -o /tmp/server.schema.json - name: Install server.json validator dependencies diff --git a/server.json b/server.json index dac421f7e..f09429534 100644 --- a/server.json +++ b/server.json @@ -1,6 +1,6 @@ { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", - "name": "io.github.leanercloud/cudly-mcp", + "name": "io.github.LeanerCloud/cudly-mcp", "description": "Search and buy AWS/Azure/GCP reserved capacity (RIs, Savings Plans, CUDs) via any MCP client.", "repository": { "url": "https://github.com/LeanerCloud/CUDly", From 26257a19d97fcbbf7584c53a6c9ec586f274f608 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Mon, 31 Aug 2026 13:05:08 +0200 Subject: [PATCH 5/6] fix(mcp): remove unsupported registry metadata --- .github/workflows/mcp-server-json.yml | 9 ++++----- server.json | 7 ------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/workflows/mcp-server-json.yml b/.github/workflows/mcp-server-json.yml index 6979e9f93..ccce364d7 100644 --- a/.github/workflows/mcp-server-json.yml +++ b/.github/workflows/mcp-server-json.yml @@ -1,10 +1,9 @@ name: MCP server.json -# Validates server.json (the MCP Registry listing manifest, see -# docs/plans/mcp/05-store.md Phase B) on every PR that touches it, and gates -# tag pushes on server.json's version matching the tag -- the registry -# rejects republishing a version, so a mismatch here must fail loud rather -# than let release.yml silently publish the wrong metadata. +# Validates server.json (the MCP Registry listing manifest) on every PR that +# touches it, and gates tag pushes on server.json's version matching the tag. +# The registry rejects republishing a version, so a mismatch here must fail +# loud rather than let release.yml silently publish the wrong metadata. on: pull_request: diff --git a/server.json b/server.json index f09429534..ed972ca42 100644 --- a/server.json +++ b/server.json @@ -23,13 +23,6 @@ "isRequired": false, "isSecret": false, "format": "string" - }, - { - "name": "CUDLY_MCP_AUDIT_LOG", - "description": "Overrides the JSONL audit log path for purchase attempts; set to an empty string to disable audit logging. See mcp/README.md for the current default path.", - "isRequired": false, - "isSecret": false, - "format": "filepath" } ] } From 235058c4fa8c8b6af615b7432530a119ba5bf763 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Mon, 31 Aug 2026 13:49:54 +0200 Subject: [PATCH 6/6] fix(mcp): harden registry validation workflow --- .github/workflows/mcp-server-json.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/mcp-server-json.yml b/.github/workflows/mcp-server-json.yml index ccce364d7..efc464a15 100644 --- a/.github/workflows/mcp-server-json.yml +++ b/.github/workflows/mcp-server-json.yml @@ -3,7 +3,7 @@ name: MCP server.json # Validates server.json (the MCP Registry listing manifest) on every PR that # touches it, and gates tag pushes on server.json's version matching the tag. # The registry rejects republishing a version, so a mismatch here must fail -# loud rather than let release.yml silently publish the wrong metadata. +# loud rather than let release automation publish the wrong metadata. on: pull_request: @@ -44,7 +44,8 @@ jobs: echo "::error::server.json name must be $expected_registry_name" exit 1 fi - curl -sSfL "$expected_schema_url" -o /tmp/server.schema.json + curl -sSfL --connect-timeout 10 --max-time 30 \ + "$expected_schema_url" -o /tmp/server.schema.json - name: Install server.json validator dependencies working-directory: .github/scripts/server-json-validator @@ -72,8 +73,8 @@ jobs: server_version=$(jq -r '.version' server.json) if [[ "$tag" != "$server_version" ]]; then echo "::error::server.json version ($server_version) does not match tag v$tag." \ - "Bump server.json's version (and, for the MCPB package entry," \ - "packages[].identifier and packages[].fileSha256) in the release PR before tagging -- this never rewrites the file for you." + "Correct server.json.version in the release commit before creating or recreating the tag;" \ + "this workflow never rewrites the file." exit 1 fi echo "server.json version ($server_version) matches tag v$tag"