Plugin structure and testing changedplugins/build
Nearest release: v2.1.283, published under an hour after upstream edited the page. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.
Upstream edited this page at 25 Sep 2026 18:00 UTC, give or take a minute or two: the time comes from Anthropic’s own sitemap rather than from a commit. This site recorded the change at 25 Sep 2026 18:07 UTC.
# Plugin structure and testing ## Lay out the plugin folder ### Folder layout ### Check what each app loads ### Start from an example ## Write the plugin ### Write the manifest ### Bundle an MCP connector with its skill ## Test the plugin on each surface ## Add Claude Code-only components ## Next steps
The whole hunk
642 lines, new pageA whole new page. There's nothing to diff it against, so here is what it says.
# Plugin structure and testing
> Reference for the plugin folder: what each file is, the manifest fields every app reads, bundling an MCP connector with its skill, and testing on each app.
export const Piece = ({id, children}) => <div className="pe-piece" data-piece={id}>{children}</div>;
export const PluginExplorer = ({children, variant}) => {
const SKILL_PIECES = [{
id: 'skillmd',
required: 'Required',
name: 'SKILL.md',
path: 'brand-guidelines/SKILL.md',
lines: [{
depth: 0,
kind: 'file',
text: 'SKILL.md'
}],
href: '/skills/how-to#create-a-skillmd-file',
linkText: 'Go to Create a SKILL.md file'
}, {
id: 'references',
name: 'Reference file',
path: 'brand-guidelines/references/voice-and-tone.md',
lines: [{
depth: 0,
kind: 'folder',
text: 'references/'
}, {
depth: 1,
kind: 'file',
text: 'voice-and-tone.md'
}],
href: '/skills/how-to#add-resources',
linkText: 'Go to Add resources'
}, {
id: 'assets',
name: 'Asset',
path: 'brand-guidelines/assets/slide-template.md',
lines: [{
depth: 0,
kind: 'folder',
text: 'assets/'
}, {
depth: 1,
kind: 'file',
text: 'slide-template.md'
}],
href: '/skills/how-to#add-resources',
linkText: 'Go to Add resources'
}, {
id: 'scripts',
name: 'Script',
path: 'brand-guidelines/scripts/check_contrast.py',
lines: [{
depth: 0,
kind: 'folder',
text: 'scripts/'
}, {
depth: 1,
kind: 'file',
text: 'check_contrast.py'
}],
href: '/skills/how-to#add-scripts',
linkText: 'Go to Add scripts'
}];
const PLUGIN_PIECES = [{
id: 'manifest',
required: 'Required',
name: 'Manifest',
path: '.claude-plugin/plugin.json',
lines: [{
depth: 0,
kind: 'folder',
text: '.claude-plugin/'
}, {
depth: 1,
kind: 'file',
text: 'plugin.json'
}],
href: '/plugins/build#write-the-manifest',
linkText: 'Go to Write the manifest'
}, {
id: 'skills',
name: 'Skill',
path: 'skills/file-expense/SKILL.md',
lines: [{
depth: 0,
kind: 'folder',
text: 'skills/'
}, {
depth: 1,
kind: 'folder',
text: 'file-expense/'
}, {
depth: 2,
kind: 'file',
text: 'SKILL.md'
}],
href: '/skills/how-to',
linkText: 'Go to Create custom skills'
}, {
id: 'references',
name: 'Skill reference file',
path: 'skills/file-expense/references/categories.md',
lines: [{
depth: 2,
kind: 'folder',
text: 'references/'
}, {
depth: 3,
kind: 'file',
text: 'categories.md'
}],
href: '/skills/how-to#add-resources',
linkText: 'Go to Add resources'
}, {
id: 'scripts',
name: 'Skill script',
path: 'skills/file-expense/scripts/total.py',
lines: [{
depth: 2,
kind: 'folder',
text: 'scripts/'
}, {
depth: 3,
kind: 'file',
text: 'total.py'
}],
href: '/skills/how-to#add-scripts',
linkText: 'Go to Add scripts'
}, {
id: 'commands',
name: 'Command',
path: 'commands/summarize.md',
lines: [{
depth: 0,
kind: 'folder',
text: 'commands/'
}, {
depth: 1,
kind: 'file',
text: 'summarize.md'
}],
href: '/plugins/platform-support#compare-component-support-by-app',
linkText: 'Go to component support by app'
}, {
id: 'mcp',
name: 'MCP connector',
path: '.mcp.json',
lines: [{
depth: 0,
kind: 'file',
text: '.mcp.json'
}],
href: '/plugins/build#bundle-an-mcp-connector-with-its-skill',
linkText: 'Go to Bundle an MCP connector with its skill'
}, {
id: 'readme',
required: 'Required to publish',
name: 'README',
path: 'README.md',
lines: [{
depth: 0,
kind: 'file',
text: 'README.md'
}],
href: '/plugins/pre-submission-checklist#readme-and-license',
linkText: 'Go to README and license checks'
}, {
id: 'license',
required: 'Required to publish',
name: 'License',
path: 'LICENSE',
lines: [{
depth: 0,
kind: 'file',
text: 'LICENSE'
}],
href: '/plugins/pre-submission-checklist#readme-and-license',
linkText: 'Go to README and license checks'
}];
const isSkill = variant === 'skill';
const PIECES = isSkill ? SKILL_PIECES : PLUGIN_PIECES;
const rootLabel = isSkill ? 'brand-guidelines/' : 'expense-reports/';
const title = isSkill ? 'What goes in the skill folder' : 'What goes in the plugin folder';
const treeCaption = isSkill ? 'Skill folder' : 'Plugin folder';
const [selectedId, setSelectedId] = useState(isSkill ? 'skillmd' : 'manifest');
const [isFullscreen, setIsFullscreen] = useState(false);
const rootRef = useRef(null);
useEffect(() => {
const onFsChange = () => setIsFullscreen(!!document.fullscreenElement);
document.addEventListener('fullscreenchange', onFsChange);
return () => document.removeEventListener('fullscreenchange', onFsChange);
}, []);
const toggleFullscreen = () => {
if (!rootRef.current) return;
if (document.fullscreenElement) document.exitFullscreen(); else rootRef.current.requestFullscreen().catch(() => {});
};
const selected = PIECES.find(p => p.id === selectedId) || PIECES[0];
const onTreeKeyDown = e => {
const keys = ['ArrowDown', 'ArrowUp', 'Home', 'End'];
if (keys.indexOf(e.key) === -1) return;
const i = PIECES.findIndex(p => p.id === selectedId);
let next = i;
if (e.key === 'ArrowDown') next = Math.min(PIECES.length - 1, i + 1);
if (e.key === 'ArrowUp') next = Math.max(0, i - 1);
if (e.key === 'Home') next = 0;
if (e.key === 'End') next = PIECES.length - 1;
e.preventDefault();
if (next === i) return;
const id = PIECES[next].id;
setSelectedId(id);
const el = document.getElementById('pe-node-' + id);
if (el) el.focus();
};
const FolderIcon = () => <svg className="pe-icon" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.3" strokeLinejoin="round" aria-hidden="true">
<path d="M1.5 4.5a1 1 0 0 1 1-1h3.2l1.3 1.5h6a1 1 0 0 1 1 1V12a1 1 0 0 1-1 1h-10.5a1 1 0 0 1-1-1z" />
</svg>;
const FileIcon = () => <svg className="pe-icon" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.3" strokeLinejoin="round" aria-hidden="true">
<path d="M4 1.5h5.5L13 5v9.5H4z" />
<path d="M9.5 1.5V5H13" />
</svg>;
return <div ref={rootRef} className={isFullscreen ? 'pe-root pe-fullscreen not-prose' : 'pe-root not-prose'} data-selected={selected.id}>
<style>{`
.pe-root {
--pe-mono: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
--pe-accent: #D97757;
--pe-accent-text: #A8502F;
--pe-accent-bg: rgba(217,119,87,0.10);
--pe-bg: #FFFFFF;
--pe-surface: #FAFAF7;
--pe-hover: #F0EEE6;
--pe-border: #E8E6DC;
--pe-text: #141413;
--pe-text-2: #3D3D3A;
--pe-text-3: #5E5D59;
font-family: inherit;
background: var(--pe-bg);
color: var(--pe-text);
border: 1px solid var(--pe-border);
border-radius: 12px;
margin: 1.5rem 0;
overflow: hidden;
box-sizing: border-box;
}
.dark .pe-root {
--pe-accent-text: #EBA98F;
--pe-accent-bg: rgba(217,119,87,0.18);
--pe-bg: #1A1918;
--pe-surface: #232221;
--pe-hover: #2E2D2B;
--pe-border: #3A3936;
--pe-text: #F1EFE9;
--pe-text-2: #D6D4CA;
--pe-text-3: #B8B5AD;
}
.pe-root *, .pe-root *::before, .pe-root *::after { box-sizing: border-box; }
.pe-head { display: flex; align-items: flex-start; gap: 12px; padding: 18px 24px 16px; border-bottom: 1px solid var(--pe-border); }
.pe-head-text { flex: 1; min-width: 0; }
.pe-fs-btn { flex-shrink: 0; width: 32px; height: 32px; display: inline-flex; align-items: center; justify-content: center; border: 1px solid var(--pe-border); border-radius: 6px; background: var(--pe-surface); color: var(--pe-text-2); font-size: 15px; line-height: 1; cursor: pointer; }
.pe-fs-btn:hover { background: var(--pe-hover); }
.pe-fs-btn:focus-visible { outline: 2px solid var(--pe-accent); outline-offset: 2px; }
.pe-fullscreen { border-radius: 0; height: 100vh; display: flex; flex-direction: column; overflow: auto; }
.pe-fullscreen .pe-body { flex: 1; }
.pe-title { font-size: 19px; font-weight: 600; line-height: 1.3; color: var(--pe-text); margin: 0; }
.pe-sub { font-size: 15px; line-height: 1.5; color: var(--pe-text-3); margin: 4px 0 0; }
.pe-sub code { font-family: var(--pe-mono); font-size: 0.88em; padding: 1px 5px; border-radius: 4px; background: var(--pe-surface); border: 1px solid var(--pe-border); }
.pe-body { display: flex; align-items: stretch; }
.pe-tree-pane { width: 270px; flex-shrink: 0; background: var(--pe-surface); border-right: 1px solid var(--pe-border); padding: 16px 0 12px; }
.pe-panel { flex: 1; min-width: 0; padding: 16px 24px 24px; }
.pe-caption { font-size: 13px; font-weight: 600; color: var(--pe-text-3); margin: 0 0 10px; }
.pe-tree-pane .pe-caption { padding: 0 16px; }
.pe-rootline { display: flex; align-items: center; gap: 7px; padding: 3px 16px; font-family: var(--pe-mono); font-size: 13.5px; color: var(--pe-text-3); }
.pe-node {
display: block; width: 100%; margin: 0; padding: 3px 16px 3px 30px; text-align: left; cursor: pointer;
background: transparent; color: var(--pe-text-2);
border: none; border-left: 3px solid transparent;
font-family: var(--pe-mono); font-size: 13.5px; line-height: 1.4;
}
.pe-node:hover { background: var(--pe-hover); }
.pe-node:focus-visible { outline: 2px solid var(--pe-accent); outline-offset: -2px; }
.pe-node[aria-pressed="true"] { background: var(--pe-accent-bg); border-left-color: var(--pe-accent); color: var(--pe-accent-text); font-weight: 600; }
.pe-line { display: flex; align-items: center; gap: 7px; padding: 2px 0; }
.pe-line span { overflow-wrap: anywhere; }
.pe-line > span:not(.pe-req) { white-space: nowrap; flex-shrink: 0; }
.pe-req { flex-shrink: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; margin-left: 8px; padding: 0 6px; border-radius: 999px; font-size: 11px; line-height: 18px; font-family: var(--pe-sans, inherit); letter-spacing: .02em; color: var(--pe-accent-text); border: 1px solid var(--pe-border); background: var(--pe-surface); white-space: nowrap; }
.pe-piece { display: none; font-size: 16px; line-height: 1.6; color: var(--pe-text-2); }
${PIECES.map(p => '.pe-root[data-selected="' + p.id + '"] .pe-piece[data-piece="' + p.id + '"]').join(',\n ')} { display: block; }
.pe-piece p { margin: 0 0 10px; }
.pe-piece p:last-child { margin-bottom: 0; }
.pe-piece ul { list-style: disc; padding-left: 1.25em; margin: 0 0 10px; }
.pe-piece li { margin: 2px 0; }
.pe-piece code { font-family: var(--pe-mono); font-size: 0.88em; padding: 1px 5px; border-radius: 4px; background: var(--pe-surface); border: 1px solid var(--pe-border); }
.pe-piece .code-block { margin: 12px 0 0; }
.pe-piece pre code { padding: 0; border: none; background: none; }
.pe-piece a { color: var(--pe-accent-text); }
.pe-line-compact { display: none; }
.pe-icon { flex-shrink: 0; }
.pe-name { font-size: 22px; font-weight: 600; line-height: 1.25; letter-spacing: -0.2px; color: var(--pe-text); margin: 0; }
.pe-path { font-family: var(--pe-mono); font-size: 13.5px; color: var(--pe-accent-text); margin: 4px 0 0; overflow-wrap: anywhere; }
Cut at 300 lines. The page has the rest.
No line in this hunk matches that.