Skip to content
Open
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
11 changes: 11 additions & 0 deletions .github/workflows/sync-agent-context.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
12 changes: 7 additions & 5 deletions agent-context/README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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:
Expand All @@ -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.

Expand All @@ -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.

Expand Down
15 changes: 12 additions & 3 deletions agent-context/scripts/check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
),
),
Expand Down
63 changes: 56 additions & 7 deletions agent-context/scripts/lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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'),
Expand Down
33 changes: 33 additions & 0 deletions agent-context/targets/kiro.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
76 changes: 75 additions & 1 deletion agent-context/test/build.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
);
Expand All @@ -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/);
Expand All @@ -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',
Expand All @@ -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');
Expand Down