Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions .github/prompts/update-mcp-coverage.md
Original file line number Diff line number Diff line change
@@ -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`.
12 changes: 12 additions & 0 deletions .github/scripts/mcp-update-disallowed-paths.sh
Original file line number Diff line number Diff line change
@@ -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
113 changes: 113 additions & 0 deletions .github/tests/update-coverage-security.test.mjs
Original file line number Diff line number Diff line change
@@ -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\(\)/);
}
});
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading