Follow Discord
Sweep 25 Sep 2026 · 19:33Z Build v2.1.283 504 read Stable v2.1.274 Latest v2.1.283 Next v2.1.283 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One change · claude-docs

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.

Upstream edited
Recorded here
Lines+642added
Lines−0removed
From line — no hunk to open at
First seen 25 Sep 2026 this site's first read of the page
Recorded edits3to this page, all time

# 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 page
/
lines

A 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.

Feedback