From bf1e70348144387fb341c2673e5f6723a0d760b7 Mon Sep 17 00:00:00 2001 From: mayankshouche Date: Fri, 7 Aug 2026 17:24:54 -0700 Subject: [PATCH] feat: generate kiro power in agent-context --- .github/workflows/sync-agent-context.yml | 11 ++++ agent-context/README.md | 12 ++-- agent-context/scripts/check.mjs | 15 ++++- agent-context/scripts/lib.mjs | 63 +++++++++++++++++--- agent-context/targets/kiro.json | 33 ++++++++++ agent-context/test/build.test.mjs | 76 +++++++++++++++++++++++- 6 files changed, 194 insertions(+), 16 deletions(-) create mode 100644 agent-context/targets/kiro.json diff --git a/.github/workflows/sync-agent-context.yml b/.github/workflows/sync-agent-context.yml index 790e258002..29218fcd3a 100644 --- a/.github/workflows/sync-agent-context.yml +++ b/.github/workflows/sync-agent-context.yml @@ -44,14 +44,22 @@ jobs: repository: mintlify/codex-plugin repository_name: codex-plugin mcp_file: .mcp.json + manifest_file: "" - target: cursor repository: mintlify/cursor-plugin repository_name: cursor-plugin mcp_file: mcp.json + manifest_file: "" - target: claude repository: mintlify/mintlify-claude-plugin repository_name: mintlify-claude-plugin mcp_file: .mcp.json + manifest_file: "" + - target: kiro + repository: mintlify/kiro-power + repository_name: kiro-power + mcp_file: mcp.json + manifest_file: plugin.json steps: - name: Check out context source @@ -106,6 +114,9 @@ jobs: git config user.email "${APP_SLUG}[bot]@users.noreply.github.com" git checkout -B "$BRANCH" git add skills/mintlify .mintlify-agent-context.json "${{ matrix.mcp_file }}" + if [[ -n "${{ matrix.manifest_file }}" ]]; then + git add "${{ matrix.manifest_file }}" + fi git commit -m "Sync Mintlify agent context" git fetch origin "$BRANCH:refs/remotes/origin/$BRANCH" || true git push --force-with-lease origin "HEAD:$BRANCH" diff --git a/agent-context/README.md b/agent-context/README.md index 62c3453630..7845dbd1f8 100644 --- a/agent-context/README.md +++ b/agent-context/README.md @@ -1,17 +1,17 @@ # Mintlify agent context -Single source of truth, maintained in the Mintlify documentation repository, for the Mintlify skill distributed through the Codex, Cursor, and Claude plugins. +Single source of truth, maintained in the Mintlify documentation repository, for the Mintlify skill distributed through the Codex, Cursor, and Claude plugins and the Kiro power. ## Repository structure - `context/skills/mintlify/` contains canonical, client-neutral context. - `context/mcp-servers.json` contains canonical MCP names, URLs, and transport settings. -- `targets/*.json` contains only client packaging differences such as MCP config file and schema names. +- `targets/*.json` contains only client packaging differences such as MCP config and skill directory conventions. The Kiro target also contains its required Agent Plugins manifest. - `scripts/build.mjs` renders self-contained plugin artifacts into `dist/`. - `scripts/sync-target.mjs` replaces only `skills/mintlify/` in a target repository. -- `../.github/workflows/sync-agent-context.yml` opens generated sync pull requests in all three plugin repositories. +- `../.github/workflows/sync-agent-context.yml` opens generated sync pull requests in all four target repositories. -Plugin manifests, assets, READMEs, and Cursor rules remain owned by their target repositories. This project generates the shared skill and each client's MCP configuration file. +Plugin manifests, assets, READMEs, and Cursor rules remain owned by their target repositories, except for Kiro's required `plugin.json`, which is generated from its target configuration. This project generates the shared skill and each client's MCP configuration file. ## Local development @@ -28,6 +28,7 @@ Build one target by passing its ID: ```bash node scripts/build.mjs codex +node scripts/build.mjs kiro ``` Preview a sync into a local checkout: @@ -37,7 +38,7 @@ node scripts/sync-target.mjs codex ../../codex-plugin git -C ../../codex-plugin diff ``` -The sync command replaces `skills/mintlify/`, writes the client-specific MCP configuration file, and writes `.mintlify-agent-context.json` with the source commit. It does not change any other plugin files. +The sync command replaces `skills/mintlify/`, writes the client-specific MCP configuration file, and writes `.mintlify-agent-context.json` with the source commit. For Kiro, it also writes the required `plugin.json`. It does not change any other plugin files. `npm run status` compares locally checked-out sibling plugin repositories with fresh builds and reports whether each one is current. Pass a workspace root as the final argument if the repositories do not share this repository's parent directory. @@ -48,6 +49,7 @@ Create a GitHub App installed on these repositories: - `mintlify/codex-plugin` - `mintlify/cursor-plugin` - `mintlify/mintlify-claude-plugin` +- `mintlify/kiro-power` Grant the app repository **Contents: read and write** and **Pull requests: read and write** permissions. Add its client ID as the `CONTEXT_SYNC_APP_CLIENT_ID` Actions variable and its private key as the `CONTEXT_SYNC_APP_PRIVATE_KEY` Actions secret in the `mintlify/docs` repository. diff --git a/agent-context/scripts/check.mjs b/agent-context/scripts/check.mjs index fcb4b4197f..85e125b1c1 100644 --- a/agent-context/scripts/check.mjs +++ b/agent-context/scripts/check.mjs @@ -2,13 +2,15 @@ import assert from 'node:assert/strict'; import { mkdtemp, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; -import { buildAll } from './lib.mjs'; +import { buildAll, loadTargets } from './lib.mjs'; const outputRoot = await mkdtemp(path.join(tmpdir(), 'mintlify-agent-context-')); try { const results = await buildAll({ outputRoot }); - assert.equal(results.length, 3); + const targets = await loadTargets(); + const targetsById = new Map(targets.map((target) => [target.id, target])); + assert.equal(results.length, 4); const sharedFiles = [ 'api-docs.md', @@ -22,7 +24,14 @@ try { const contents = await Promise.all( results.map(({ provenance }) => readFile( - path.join(outputRoot, provenance.target, 'skills', 'mintlify', 'reference', file), + path.join( + outputRoot, + provenance.target, + 'skills', + 'mintlify', + targetsById.get(provenance.target).skillReferenceDirectory ?? 'reference', + file, + ), 'utf8', ), ), diff --git a/agent-context/scripts/lib.mjs b/agent-context/scripts/lib.mjs index 8bcd2a8e6f..eda70422e1 100644 --- a/agent-context/scripts/lib.mjs +++ b/agent-context/scripts/lib.mjs @@ -25,10 +25,32 @@ export async function loadTargets(selectedIds = []) { typeof target.id !== 'string' || typeof target.repository !== 'string' || !['.mcp.json', 'mcp.json'].includes(target.mcpConfigFile) || - !['mcp_servers', 'mcpServers'].includes(target.mcpConfigKey) + !['mcp_servers', 'mcpServers'].includes(target.mcpConfigKey) || + (target.skillReferenceDirectory !== undefined && + !['reference', 'references'].includes(target.skillReferenceDirectory)) || + (target.mcpSchema !== undefined && typeof target.mcpSchema !== 'string') || + (target.mcpTypeOverrides !== undefined && + (target.mcpTypeOverrides === null || + typeof target.mcpTypeOverrides !== 'object' || + Object.values(target.mcpTypeOverrides).some((value) => typeof value !== 'string'))) ) { throw new Error(`Invalid target configuration: ${JSON.stringify(target)}`); } + + if (target.pluginManifest !== undefined) { + const manifest = target.pluginManifest; + if ( + manifest?.$schema !== 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json' || + typeof manifest.name !== 'string' || + typeof manifest.version !== 'string' || + typeof manifest.description !== 'string' || + typeof manifest.author?.name !== 'string' || + !Array.isArray(manifest.keywords) || + manifest.keywords.length === 0 + ) { + throw new Error(`Invalid plugin manifest: ${JSON.stringify(manifest)}`); + } + } } const ids = new Set(targets.map((target) => target.id)); @@ -93,24 +115,48 @@ function validateSkill(skill, target) { export async function buildTarget(target, outputRoot) { const targetRoot = path.join(outputRoot, target.id); const skillOutput = path.join(targetRoot, 'skills', 'mintlify'); + const referenceDirectory = target.skillReferenceDirectory ?? 'reference'; await rm(targetRoot, { recursive: true, force: true }); await mkdir(skillOutput, { recursive: true }); const skillTemplate = await readFile(path.join(contextDirectory, 'SKILL.md'), 'utf8'); - const skill = markGenerated(skillTemplate); + const skill = markGenerated(skillTemplate).replaceAll( + 'reference/', + `${referenceDirectory}/`, + ); validateSkill(skill, target); await writeFile(path.join(skillOutput, 'SKILL.md'), skill); - await cp(path.join(contextDirectory, 'reference'), path.join(skillOutput, 'reference'), { - recursive: true, - }); + await cp( + path.join(contextDirectory, 'reference'), + path.join(skillOutput, referenceDirectory), + { recursive: true }, + ); - const mcpServers = JSON.parse(await readFile(mcpServersPath, 'utf8')); - const mcpConfig = { [target.mcpConfigKey]: mcpServers }; + const canonicalMcpServers = JSON.parse(await readFile(mcpServersPath, 'utf8')); + const mcpServers = Object.fromEntries( + Object.entries(canonicalMcpServers).map(([name, server]) => [ + name, + target.mcpTypeOverrides?.[server.type] === undefined + ? server + : { ...server, type: target.mcpTypeOverrides[server.type] }, + ]), + ); + const mcpConfig = { + ...(target.mcpSchema === undefined ? {} : { $schema: target.mcpSchema }), + [target.mcpConfigKey]: mcpServers, + }; await writeFile( path.join(targetRoot, target.mcpConfigFile), `${JSON.stringify(mcpConfig, null, 2)}\n`, ); + if (target.pluginManifest !== undefined) { + await writeFile( + path.join(targetRoot, 'plugin.json'), + `${JSON.stringify(target.pluginManifest, null, 2)}\n`, + ); + } + const provenance = { schemaVersion: 1, sourceRepository: 'mintlify/docs', @@ -147,6 +193,9 @@ export async function copyTargetToRepository(targetId, destination, outputRoot) path.join(sourceRoot, target.mcpConfigFile), path.join(destination, target.mcpConfigFile), ); + if (target.pluginManifest !== undefined) { + await cp(path.join(sourceRoot, 'plugin.json'), path.join(destination, 'plugin.json')); + } await cp( path.join(sourceRoot, '.mintlify-agent-context.json'), path.join(destination, '.mintlify-agent-context.json'), diff --git a/agent-context/targets/kiro.json b/agent-context/targets/kiro.json new file mode 100644 index 0000000000..7da3d3016f --- /dev/null +++ b/agent-context/targets/kiro.json @@ -0,0 +1,33 @@ +{ + "id": "kiro", + "repository": "mintlify/kiro-power", + "skillReferenceDirectory": "references", + "mcpConfigFile": "mcp.json", + "mcpConfigKey": "mcpServers", + "mcpSchema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "mcpTypeOverrides": { + "http": "streamable-http" + }, + "pluginManifest": { + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "mintlify", + "version": "1.0.0", + "description": "Create, maintain, and improve Mintlify documentation with product guidance and Mintlify tools.", + "author": { + "name": "Mintlify", + "url": "https://mintlify.com" + }, + "keywords": [ + "mintlify", + "documentation", + "docs.json", + "api documentation", + "technical writing", + "mdx", + "openapi" + ], + "homepage": "https://mintlify.com/docs", + "repository": "https://github.com/mintlify/kiro-power", + "license": "MIT" + } +} diff --git a/agent-context/test/build.test.mjs b/agent-context/test/build.test.mjs index af2ed4710d..0a562833b3 100644 --- a/agent-context/test/build.test.mjs +++ b/agent-context/test/build.test.mjs @@ -22,6 +22,10 @@ test('builds all client variants from one canonical skill', async () => { path.join(outputRoot, 'claude', 'skills', 'mintlify', 'SKILL.md'), 'utf8', ); + const kiro = await readFile( + path.join(outputRoot, 'kiro', 'skills', 'mintlify', 'SKILL.md'), + 'utf8', + ); const codexMcp = JSON.parse( await readFile(path.join(outputRoot, 'codex', '.mcp.json'), 'utf8'), ); @@ -31,10 +35,19 @@ test('builds all client variants from one canonical skill', async () => { const claudeMcp = JSON.parse( await readFile(path.join(outputRoot, 'claude', '.mcp.json'), 'utf8'), ); + const kiroMcp = JSON.parse( + await readFile(path.join(outputRoot, 'kiro', 'mcp.json'), 'utf8'), + ); + const kiroManifest = JSON.parse( + await readFile(path.join(outputRoot, 'kiro', 'plugin.json'), 'utf8'), + ); assert.equal(cursor, codex); assert.equal(claude, codex); - for (const skill of [codex, cursor, claude]) { + assert.equal(kiro.replaceAll('references/', 'reference/'), codex); + assert.match(kiro, /`references\/components\.md`/); + assert.doesNotMatch(kiro, /`reference\//); + for (const skill of [codex, cursor, claude, kiro]) { assert.match(skill, /Generated from mintlify\/docs\/agent-context/); assert.match(skill, /### Mintlify Search/); assert.match(skill, /### Mintlify Admin/); @@ -44,6 +57,28 @@ test('builds all client variants from one canonical skill', async () => { } assert.deepEqual(codexMcp.mcp_servers, cursorMcp.mcpServers); assert.deepEqual(claudeMcp.mcpServers, cursorMcp.mcpServers); + assert.deepEqual( + Object.fromEntries( + Object.entries(kiroMcp.mcpServers).map(([name, server]) => [ + name, + { ...server, type: 'http' }, + ]), + ), + cursorMcp.mcpServers, + ); + assert.ok( + Object.values(kiroMcp.mcpServers).every((server) => server.type === 'streamable-http'), + ); + assert.equal( + kiroMcp.$schema, + 'https://agent-plugins.org/schemas/1.0.0/mcp.schema.json', + ); + assert.equal( + kiroManifest.$schema, + 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json', + ); + assert.equal(kiroManifest.name, 'mintlify'); + assert.ok(kiroManifest.keywords.includes('mintlify')); assert.deepEqual(Object.keys(cursorMcp.mcpServers), [ 'Mintlify Search', 'Mintlify Admin', @@ -53,6 +88,45 @@ test('builds all client variants from one canonical skill', async () => { } }); +test('sync writes the complete Kiro power without changing target-owned files', async () => { + const root = await mkdtemp(path.join(tmpdir(), 'mintlify-agent-context-kiro-sync-test-')); + const outputRoot = path.join(root, 'dist'); + const destination = path.join(root, 'kiro-power'); + + try { + await mkdir(destination, { recursive: true }); + await writeFile(path.join(destination, 'README.md'), 'target-owned\n'); + + await buildAll({ outputRoot, selectedIds: ['kiro'] }); + await copyTargetToRepository('kiro', destination, outputRoot); + + assert.equal(await readFile(path.join(destination, 'README.md'), 'utf8'), 'target-owned\n'); + const manifest = JSON.parse(await readFile(path.join(destination, 'plugin.json'), 'utf8')); + const mcpConfig = JSON.parse(await readFile(path.join(destination, 'mcp.json'), 'utf8')); + assert.equal(manifest.name, 'mintlify'); + assert.deepEqual(Object.keys(mcpConfig.mcpServers), [ + 'Mintlify Search', + 'Mintlify Admin', + ]); + assert.match( + await readFile(path.join(destination, 'skills', 'mintlify', 'SKILL.md'), 'utf8'), + /### Mintlify Search/, + ); + assert.match( + await readFile( + path.join(destination, 'skills', 'mintlify', 'references', 'components.md'), + 'utf8', + ), + /# Components/, + ); + await assert.rejects( + readFile(path.join(destination, 'skills', 'mintlify', 'reference', 'components.md')), + ); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + test('sync replaces only generated context paths', async () => { const root = await mkdtemp(path.join(tmpdir(), 'mintlify-agent-context-sync-test-')); const outputRoot = path.join(root, 'dist');