diff --git a/.github/prompts/update-mcp-coverage.md b/.github/prompts/update-mcp-coverage.md new file mode 100644 index 00000000..bf086d07 --- /dev/null +++ b/.github/prompts/update-mcp-coverage.md @@ -0,0 +1,90 @@ +# Update MCP coverage for the Node SDK + +You are working in the `kernel/kernel-mcp-server` checkout. Read its AGENTS.md first. +The published SDK source is in `../sdk`; the workflow has already pinned +`@onkernel/sdk` in package.json and bun.lock to the `SDK_VERSION` environment variable. +Do not downgrade or change that version. + +## Audit and implement + +1. Read `$RUNNER_TEMP/sdk-diff.patch`, `../sdk/api.md`, and the parameter types in + `../sdk/src/resources`. Enumerate SDK methods and their parameters, including + nested options, new enum values, pagination, streaming, and changed signatures. +2. Read `src/lib/mcp/register.ts`, `src/lib/mcp/tools/`, and shared MCP schemas. + Map SDK methods/parameters to registered MCP tools, actions, and input fields. + Check the entire SDK surface, not just the release diff, for missing coverage. +3. Implement missing user-facing capabilities using existing grouped tools and + action dispatch patterns. Add new toolsets to the registration when necessary. + Update input schemas, descriptions, request construction, response handling, + pagination, and tests together. Each addition in the SDK diff must be covered + or explicitly listed as intentionally excluded with a concrete reason. +4. Preserve existing authentication, project selection, entitlement checks, + client-capability gates, secret handling, and error/response conventions. + Internal hosted-login callbacks and transport-specific APIs are not automatically + appropriate MCP tools. Do not expose secrets or internal endpoints just to claim + complete coverage. Preserve deliberate exclusions and document them. +5. Add mocked tests for new actions, fields, and changed behavior, following nearby + tests. Verify input schemas and forwarded SDK arguments, including false/zero + values. Do not make production API calls or create real resources. + +## Sync descriptions with the API spec + +The Kernel API spec as of this SDK release is at +`$RUNNER_TEMP/kernel-api/packages/api/openapi.yaml`, and `$RUNNER_TEMP/openapi-diff.patch` +shows spec changes between the previous SDK release and this one. The SDK is generated +from this spec, so its endpoint, parameter, and schema descriptions are the source of +truth for what the API does. + +1. For every changed `summary` or `description` in the spec diff that belongs to an + endpoint, parameter, or field the MCP server exposes, update the matching tool + description, action description, or input `.describe()` text. Use the spec text + for anything you add in this run too. +2. Adapt the text; do not copy it verbatim. API descriptions are written for HTTP + callers and often reference other endpoints, HTTP methods, status codes, + headers, or JSON body paths that mean nothing to an MCP client. Rewrite those + references in MCP terms: name the tool and action that does the equivalent and + use the tool's parameter names. For example, "List connections with + `GET /auth/connections`" becomes "list connections with manage_auth_connections + action `list`". Drop API details that do not apply to the tool, and follow the + existing description style (lowercase, concise, action prefixes like + "(create)"). +3. Preserve meaning exactly: limits, defaults, units, accepted values, deprecations, + and caveats must match the spec. Do not invent behavior the spec does not state. +4. Change only text whose meaning the spec change affects, or existing text the + spec now shows to be wrong. Do not restyle unrelated descriptions. + +## Validate and report + +Use Bun 1.3.3 for all MCP dependency and test commands. Run: + +- `bun run check:managed-auth-app` +- `bunx tsc --noEmit --incremental false` +- `bun test` + +Redis is available on localhost:6379, matching MCP CI. Do not disable tests or weaken +checks to make this update pass. Fix failures caused by your changes. Do not edit +unrelated code, workflows, generated App bundles, or authentication configuration. + +Read the complete diff against origin/main before finishing. Remove unnecessary +comments, type casts, defensive wrappers, and unrelated changes. + +Write `$RUNNER_TEMP/mcp-coverage-report.md` with concise sections describing: + +- Added or updated MCP tools, actions, and fields, with their SDK counterparts. +- Descriptions updated to match spec changes, with the spec operation or field each + one follows. +- Intentional exclusions and reasons. +- Any validation limitations. + +If no gaps exist, explicitly say that the complete SDK method/parameter audit found +no missing user-facing coverage. Never claim tests passed unless you ran them. +The report becomes part of a public PR: include no credentials, customer information, +private discussions, or internal URLs. + +Do not commit, push, open or merge a PR. The workflow validates the changes and +opens or updates the evergreen PR only after all checks pass. + +Only `package.json`, `bun.lock`, and `.ts`/`.tsx` files under `src/` (excluding +`src/lib/mcp/apps/generated/`) are published; the workflow discards every other +change before validation. Keep scratch files, logs, and command output outside the +checkout, under `$RUNNER_TEMP`. diff --git a/.github/scripts/mcp-update-disallowed-paths.sh b/.github/scripts/mcp-update-disallowed-paths.sh new file mode 100755 index 00000000..4215f2cd --- /dev/null +++ b/.github/scripts/mcp-update-disallowed-paths.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Reads NUL-delimited kernel-mcp-server paths on stdin and prints, NUL-delimited, +# the ones the MCP coverage updater must not publish. +set -euo pipefail + +while IFS= read -r -d '' path; do + case "$path" in + src/lib/mcp/apps/generated/*) ;; + package.json | bun.lock | src/*.ts | src/*.tsx) continue ;; + esac + printf '%s\0' "$path" +done diff --git a/.github/tests/update-coverage-security.test.mjs b/.github/tests/update-coverage-security.test.mjs new file mode 100644 index 00000000..3d827776 --- /dev/null +++ b/.github/tests/update-coverage-security.test.mjs @@ -0,0 +1,113 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const workflow = readFileSync(new URL('../workflows/update-mcp-coverage.yml', import.meta.url), 'utf8'); +const jobs = Object.fromEntries( + workflow + .slice(workflow.indexOf('\njobs:\n')) + .split(/^ (?=[\w-]+:\n)/m) + .slice(1) + .map((job) => [job.slice(0, job.indexOf(':')), job.split(/(?=^ - )/m).slice(1)]), +); +const steps = Object.values(jobs).flat(); +const step = (job, name) => { + const value = jobs[job].find((entry) => entry.startsWith(` - name: ${name}\n`)); + assert.ok(value, `missing ${job} step: ${name}`); + return value; +}; +const disallowedPaths = (paths) => + spawnSync('bash', [new URL('../scripts/mcp-update-disallowed-paths.sh', import.meta.url).pathname], { + input: paths.map((path) => `${path}\0`).join(''), + encoding: 'utf8', + }) + .stdout.split('\0') + .filter(Boolean); + +test('actions are pinned and checkouts do not persist credentials', () => { + for (const entry of steps.filter((entry) => entry.includes('uses: '))) { + assert.match(entry, /uses: [\w/-]+@[a-f0-9]{40}(?:\s|$)/); + if (entry.includes('uses: actions/checkout@')) { + assert.match(entry, /persist-credentials: false/); + assert.doesNotMatch(entry, /token:/); + } + } +}); + +test('API source access is read-only and revoked before generation', () => { + const readToken = step('generate', 'Generate API read token'); + assert.match(readToken, /repositories: kernel\n/); + assert.match(readToken, /permission-contents: read/); + assert.doesNotMatch(readToken, /permission-[\w-]+: write/); + const fetch = step('generate', 'Fetch API spec'); + assert.match(fetch, /GH_TOKEN: \$\{\{ steps\.api-read-token\.outputs\.token \}\}/); + assert.match(fetch, /gh api --method DELETE \/installation\/token\s*$/); + assert.match(fetch, /-f until="\$1"/); + assert.doesNotMatch(fetch, /gh repo clone/); + assert.ok( + jobs.generate.indexOf(fetch) < + jobs.generate.indexOf(step('generate', 'Update SDK dependency and MCP coverage')), + ); +}); + +test('changes are restricted to the allowlist before validation', () => { + const snapshot = step('generate', 'Snapshot publishable changes'); + const verify = step('generate', 'Verify MCP changes'); + assert.ok( + jobs.generate.indexOf(step('generate', 'Update SDK dependency and MCP coverage')) < + jobs.generate.indexOf(snapshot), + ); + assert.ok(jobs.generate.indexOf(snapshot) < jobs.generate.indexOf(verify)); + assert.match(snapshot, /mcp-update-disallowed-paths\.sh/); + assert.match(snapshot, /git reset -q --hard\n\s+git clean -fdq/); + assert.match(snapshot, /AGENT_BASE: \$\{\{ steps\.prepare\.outputs\.agent_base \}\}/); + assert.match(snapshot, /git reset -q --soft "\$AGENT_BASE"\n\s+git add -A/); + assert.match(step('generate', 'Prepare update branch and SDK diff'), /agent_base=\$\(git rev-parse HEAD\)/); + const scriptCalls = workflow.match(/^.*disallowed-paths\.sh.*$/gm); + assert.equal(scriptCalls.length, 2); + for (const line of scriptCalls) assert.match(line, /\| bash /); + assert.match(verify, /has\("@onkernel\/sdk"\)/); + assert.deepEqual( + disallowedPaths([ + 'package.json', + 'bun.lock', + 'src/lib/mcp/tools/browsers.ts', + 'src/app/layout.tsx', + 'src/lib/mcp/apps/generated/managed-auth-app.ts', + 'response.json', + 'src/lib/mcp/tools/output.json', + '.github/workflows/ci.yml', + 'node_modules/x/index.ts', + ]), + [ + 'src/lib/mcp/apps/generated/managed-auth-app.ts', + 'response.json', + 'src/lib/mcp/tools/output.json', + '.github/workflows/ci.yml', + 'node_modules/x/index.ts', + ], + ); +}); + +test('publishing runs in a separate job that never executes repository code', () => { + assert.doesNotMatch(jobs.generate.join(''), /permission-[\w-]+: write/); + assert.match(step('publish', 'Generate MCP repository token'), /repositories: kernel-mcp-server\n/); + assert.match(step('publish', 'Generate MCP repository token'), /permission-contents: write/); + const publishRuns = jobs.publish.filter((entry) => entry.includes('run: |')).join(''); + assert.doesNotMatch(publishRuns, /^\s*(bun|bunx|npm|npx|node|pi|make)\b/m); + assert.doesNotMatch(jobs.publish.join(''), /secrets\.(ANTHROPIC|OPENAI)_API_KEY/); + const apply = step('publish', 'Apply validated changes'); + assert.match(apply, /git apply --index "\$RUNNER_TEMP\/mcp-coverage-update\/changes\.patch"/); + assert.match(apply, /mcp-update-disallowed-paths\.sh/); + assert.match(apply, /100644\|100755\|000000/); + const mint = jobs.publish.indexOf(step('publish', 'Generate MCP repository token')); + assert.ok(jobs.publish.indexOf(apply) < mint); + assert.doesNotMatch(jobs.publish.slice(0, mint).join(''), /GH_TOKEN:/); + const publish = step('publish', 'Open or update MCP coverage PR'); + assert.match(publish, /GH_TOKEN: \$\{\{ steps\.app-token\.outputs\.token \}\}/); + assert.match(publish, /gh auth setup-git\n\s+git push/); + for (const entry of jobs.publish) { + assert.doesNotMatch(entry, /continue-on-error: true|always\(\)/); + } +}); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03467204..3ee9bb15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,3 +92,15 @@ jobs: - name: Run tests run: ./scripts/test + + updater-security: + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '22' + - run: node --test .github/tests/update-coverage-security.test.mjs diff --git a/.github/workflows/update-mcp-coverage.yml b/.github/workflows/update-mcp-coverage.yml new file mode 100644 index 00000000..3bdea613 --- /dev/null +++ b/.github/workflows/update-mcp-coverage.yml @@ -0,0 +1,345 @@ +name: Update MCP Coverage + +on: + workflow_run: + workflows: [Publish NPM] + types: [completed] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: update-mcp-coverage + cancel-in-progress: false + +jobs: + sdk-version: + if: >- + github.repository == 'kernel/kernel-node-sdk' && + (github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.head_repository.full_name == github.repository)) + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + sha: ${{ steps.version.outputs.sha }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + ref: ${{ github.event.workflow_run.head_sha || github.sha }} + persist-credentials: false + + - name: Resolve published SDK version + id: version + run: | + version=$(jq -er 'select(.name == "@onkernel/sdk") | .version' package.json) + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Skipping non-stable SDK version: $version" + exit 0 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + generate: + needs: sdk-version + if: needs.sdk-version.outputs.version != '' + runs-on: ubuntu-latest + timeout-minutes: 55 + outputs: + changed: ${{ steps.snapshot.outputs.changed }} + main_sha: ${{ steps.prepare.outputs.main_sha }} + update_sha: ${{ steps.prepare.outputs.update_sha }} + env: + SDK_VERSION: ${{ needs.sdk-version.outputs.version }} + SDK_SHA: ${{ needs.sdk-version.outputs.sha }} + UPDATE_BRANCH: mcp-coverage-update + services: + redis: + image: redis:7-alpine + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 5s + --health-timeout 3s + --health-retries 20 + steps: + - name: Checkout updater instructions + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + + - name: Checkout published SDK + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + ref: ${{ env.SDK_SHA }} + path: sdk + fetch-depth: 0 + persist-credentials: false + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '22' + + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: '1.3.3' + + - name: Checkout MCP server + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + repository: kernel/kernel-mcp-server + path: mcp + fetch-depth: 0 + persist-credentials: false + + - name: Prepare update branch and SDK diff + id: prepare + working-directory: mcp + run: | + git config user.name 'kernel-internal[bot]' + git config user.email '260533166+kernel-internal[bot]@users.noreply.github.com' + echo "main_sha=$(git rev-parse origin/main)" >> "$GITHUB_OUTPUT" + if git show-ref --verify --quiet "refs/remotes/origin/$UPDATE_BRANCH"; then + echo "update_sha=$(git rev-parse "origin/$UPDATE_BRANCH")" >> "$GITHUB_OUTPUT" + git switch --track "origin/$UPDATE_BRANCH" + if ! git merge origin/main --no-edit; then + echo "::error::$UPDATE_BRANCH conflicts with main. Resolve the conflicts on that branch, or delete it to start fresh, then rerun this workflow." + exit 1 + fi + else + git switch -c "$UPDATE_BRANCH" origin/main + fi + echo "agent_base=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + old_version=$(jq -er '.dependencies["@onkernel/sdk"]' package.json) + if [[ "$old_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && + [[ "$old_version" != "$SDK_VERSION" ]] && + [[ "$(printf '%s\n' "$old_version" "$SDK_VERSION" | sort -V | tail -1)" == "$old_version" ]]; then + echo "MCP already uses newer SDK $old_version; skipping stale run." + exit 0 + fi + + if git -C ../sdk rev-parse --verify "refs/tags/v$old_version" >/dev/null 2>&1; then + git -C ../sdk diff "refs/tags/v$old_version" "$SDK_SHA" -- api.md src/resources > "$RUNNER_TEMP/sdk-diff.patch" + echo "old_date=$(TZ=UTC git -C ../sdk log -1 --date=format-local:%Y-%m-%dT%H:%M:%SZ --format=%cd "refs/tags/v$old_version")" >> "$GITHUB_OUTPUT" + else + printf 'Baseline %s unavailable; audit all of api.md and src/resources.\n' "$old_version" > "$RUNNER_TEMP/sdk-diff.patch" + fi + echo "new_date=$(TZ=UTC git -C ../sdk log -1 --date=format-local:%Y-%m-%dT%H:%M:%SZ --format=%cd "$SDK_SHA")" >> "$GITHUB_OUTPUT" + echo "update=true" >> "$GITHUB_OUTPUT" + + - name: Generate API read token + if: steps.prepare.outputs.update == 'true' + id: api-read-token + uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 + with: + app-id: ${{ secrets.ADMIN_APP_ID }} + private-key: ${{ secrets.ADMIN_APP_PRIVATE_KEY }} + owner: kernel + repositories: kernel + permission-contents: read + + - name: Fetch API spec + if: steps.prepare.outputs.update == 'true' + env: + GH_TOKEN: ${{ steps.api-read-token.outputs.token }} + OLD_DATE: ${{ steps.prepare.outputs.old_date }} + NEW_DATE: ${{ steps.prepare.outputs.new_date }} + run: | + main_at() { + gh api -X GET repos/kernel/kernel/commits -f sha=main -f until="$1" -f per_page=1 --jq '.[0].sha // empty' + } + file_at() { + gh api -H 'Accept: application/vnd.github.raw' "repos/kernel/kernel/contents/$1?ref=$2" + } + spec="$RUNNER_TEMP/kernel-api/packages/api/openapi.yaml" + head=$(main_at "$NEW_DATE") + test -n "$head" + mkdir -p "$(dirname "$spec")" + file_at packages/api/openapi.yaml "$head" > "$spec" + base="" + if [[ -n "$OLD_DATE" ]]; then + base=$(main_at "$OLD_DATE") + fi + if [[ -n "$base" ]]; then + file_at packages/api/openapi.yaml "$base" > "$RUNNER_TEMP/openapi-base.yaml" + diff -u --label "openapi.yaml@$base" --label "openapi.yaml@$head" \ + "$RUNNER_TEMP/openapi-base.yaml" "$spec" \ + > "$RUNNER_TEMP/openapi-diff.patch" || [[ $? -eq 1 ]] + rm "$RUNNER_TEMP/openapi-base.yaml" + else + printf 'Spec baseline unavailable; compare all relevant descriptions in openapi.yaml.\n' > "$RUNNER_TEMP/openapi-diff.patch" + fi + gh api --method DELETE /installation/token + + - name: Install Pi + if: steps.prepare.outputs.update == 'true' + env: + PI_TELEMETRY: '0' + run: | + npm install -g --ignore-scripts @earendil-works/pi-coding-agent + pi --version + + - name: Update SDK dependency and MCP coverage + if: steps.prepare.outputs.update == 'true' + working-directory: mcp + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + PI_PREFERRED_MODEL: ${{ vars.PI_PREFERRED_MODEL }} + PI_SKIP_VERSION_CHECK: '1' + PI_TELEMETRY: '0' + run: | + : "${PI_PREFERRED_MODEL:?Set the PI_PREFERRED_MODEL organization variable}" + bun add --exact "@onkernel/sdk@$SDK_VERSION" + pi --print --no-session --approve \ + --model "$PI_PREFERRED_MODEL" --thinking max \ + @../.github/prompts/update-mcp-coverage.md + + - name: Snapshot publishable changes + if: steps.prepare.outputs.update == 'true' + id: snapshot + working-directory: mcp + env: + AGENT_BASE: ${{ steps.prepare.outputs.agent_base }} + run: | + git reset -q --soft "$AGENT_BASE" + git add -A + git diff --cached --name-only --no-renames -z \ + | bash ../.github/scripts/mcp-update-disallowed-paths.sh > "$RUNNER_TEMP/excluded-paths" + if [[ -s "$RUNNER_TEMP/excluded-paths" ]]; then + echo "::warning::Discarding changes outside the MCP updater allowlist:" + tr '\0' '\n' < "$RUNNER_TEMP/excluded-paths" + GIT_LITERAL_PATHSPECS=1 git reset -q --pathspec-from-file="$RUNNER_TEMP/excluded-paths" --pathspec-file-nul + fi + git diff --cached --binary --no-renames --no-ext-diff --no-textconv > "$RUNNER_TEMP/changes.patch" + git reset -q --hard + git clean -fdq + if [[ -s "$RUNNER_TEMP/changes.patch" ]]; then + git apply --index "$RUNNER_TEMP/changes.patch" + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Verify MCP changes + if: steps.prepare.outputs.update == 'true' + working-directory: mcp + run: | + test -s "$RUNNER_TEMP/mcp-coverage-report.md" + jq -e --arg version "$SDK_VERSION" '.dependencies["@onkernel/sdk"] == $version and + ([.overrides, .resolutions, .patchedDependencies] | map((. // {}) | has("@onkernel/sdk")) | any | not)' package.json + grep -qF "\"@onkernel/sdk\": [\"@onkernel/sdk@$SDK_VERSION\", \"\"," bun.lock + bun install --frozen-lockfile + bun run check:managed-auth-app + bunx tsc --noEmit --incremental false + bun test + bun scripts/record-oauth-redis-contract.ts + git diff --exit-code -- fixtures/oauth-redis-recordings.json + git diff --cached --check + + - name: Upload validated changes + if: steps.snapshot.outputs.changed == 'true' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: mcp-coverage-update + path: | + ${{ runner.temp }}/changes.patch + ${{ runner.temp }}/mcp-coverage-report.md + if-no-files-found: error + retention-days: 7 + + publish: + needs: [sdk-version, generate] + if: needs.generate.outputs.changed == 'true' + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + SDK_VERSION: ${{ needs.sdk-version.outputs.version }} + MAIN_SHA: ${{ needs.generate.outputs.main_sha }} + UPDATE_SHA: ${{ needs.generate.outputs.update_sha }} + UPDATE_BRANCH: mcp-coverage-update + steps: + - name: Checkout updater scripts + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + + - name: Download validated changes + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: mcp-coverage-update + path: ${{ runner.temp }}/mcp-coverage-update + + - name: Checkout MCP server + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + repository: kernel/kernel-mcp-server + path: mcp + fetch-depth: 0 + persist-credentials: false + + - name: Apply validated changes + working-directory: mcp + run: | + git config user.name 'kernel-internal[bot]' + git config user.email '260533166+kernel-internal[bot]@users.noreply.github.com' + if [[ -n "$UPDATE_SHA" ]]; then + git switch -c "$UPDATE_BRANCH" "$UPDATE_SHA" + git merge --no-edit "$MAIN_SHA" + else + git switch -c "$UPDATE_BRANCH" "$MAIN_SHA" + fi + git apply --index "$RUNNER_TEMP/mcp-coverage-update/changes.patch" + git diff --cached --name-only --no-renames -z \ + | bash ../.github/scripts/mcp-update-disallowed-paths.sh > "$RUNNER_TEMP/disallowed-paths" + if [[ -s "$RUNNER_TEMP/disallowed-paths" ]]; then + echo "::error::Validated changes include paths outside the MCP updater allowlist:" + tr '\0' '\n' < "$RUNNER_TEMP/disallowed-paths" + exit 1 + fi + if git diff --cached --raw --no-renames | awk '$2 !~ /^(100644|100755|000000)$/ { found = 1 } END { exit !found }'; then + echo "::error::Validated changes may only contain regular files." + exit 1 + fi + git commit -q -m "Update Node SDK to $SDK_VERSION and MCP coverage" + + - name: Generate MCP repository token + id: app-token + uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 + with: + app-id: ${{ secrets.ADMIN_APP_ID }} + private-key: ${{ secrets.ADMIN_APP_PRIVATE_KEY }} + owner: kernel + repositories: kernel-mcp-server + permission-contents: write + permission-pull-requests: write + + - name: Open or update MCP coverage PR + working-directory: mcp + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + if git diff --quiet origin/main HEAD; then + echo "MCP coverage is already up to date." + exit 0 + fi + gh auth setup-git + git push origin "$UPDATE_BRANCH" + + body="$RUNNER_TEMP/mcp-coverage-pr.md" + { + printf '## Summary\n\n- Update `@onkernel/sdk` to `%s`.\n' "$SDK_VERSION" + printf '\n## Coverage\n\n' + head -c 60000 "$RUNNER_TEMP/mcp-coverage-update/mcp-coverage-report.md" + printf '\n## Validation\n\n- Frozen Bun install\n- Managed-auth App bundle check\n- TypeScript type check\n- Bun tests\n- OAuth Redis recording check\n' + } > "$body" + pr=$(gh pr list --repo kernel/kernel-mcp-server --head "$UPDATE_BRANCH" --base main \ + --state open --json number --jq '.[0].number // empty') + title="MCP: Update Node SDK to $SDK_VERSION" + if [[ -n "$pr" ]]; then + gh pr edit "$pr" --repo kernel/kernel-mcp-server --title "$title" --body-file "$body" + else + gh pr create --repo kernel/kernel-mcp-server --head "$UPDATE_BRANCH" --base main \ + --title "$title" --body-file "$body" + fi