---
name: museday
description: Use when building scene add-ons and templates for MarginNote 5,
  customizing study workflows, integrating plugins with templates, handling card
  data and events, or deploying add-on packages. Agents should reach for this
  skill when working on MarginNote plugin development, template configuration,
  API integration, or troubleshooting add-on behavior.
metadata:
  mintlify-proj: museday
  version: "1.0"
---

# MarginNote Developer Platform Skill

## Product summary

MarginNote is a study and annotation platform that allows customization through **scene add-ons** (web-based plugins) and **scene templates** (configuration bundles). Scene add-ons are HTML panels injected into MarginNote that communicate with the app via REST APIs. Scene templates bundle add-ons, configuration, and sample content to create complete study workflows. Use **MN Rails** (`npm create mn-rails@latest`) to scaffold projects with React, Vue, or Svelte. Key files: `mnaddon.json` (add-on manifest), `index.html` (panel entry), `template.json` (template config), `template/localizations.json` (translations). Primary API endpoint: `/bridge/v1/`. See [dev.marginnote.com.cn](https://dev.marginnote.com.cn) for full documentation.

## When to use

Reach for this skill when:
- Building a new scene add-on or template for MarginNote 5+
- Integrating an add-on with a scene template (whitelist, config, menu items)
- Reading or modifying card data, excerpts, hashtags, or add-on fields via API
- Handling events (excerpt creation, comments, card commands, sync)
- Configuring template UI (bottom toolbar, menus, floating windows)
- Debugging why add-on buttons or menu items don't appear
- Packaging and deploying `.mnaddon` or `.mntemplate` files
- Adapting panel layouts for workspace vs. mini modes

## Quick reference

### Project setup with MN Rails

```bash
npm create mn-rails@latest
# Choose: framework (React/Vue/Svelte), kind (panel/hybrid), addonid, author, title
npm run build      # Build to dist/
npm run sync       # Deploy locally to MarginNote (macOS only)
npm run package    # Create .mnaddon and .mntemplate in release/
npm run doctor     # Verify project and MarginNote environment
```

### Essential mnaddon.json fields

| Field | Type | Purpose |
|-------|------|---------|
| `addonid` | string | Unique identifier (e.g., `com.example.my-addon`) |
| `title` | string | Display name in MarginNote |
| `version` | string | Semantic version |
| `author` | string | Author name |
| `marginnote_version_min` | string | Minimum MN version (e.g., `4.5.0`) |
| `immersiveOnly` | boolean | Restrict to immersive mode (recommended: `true`) |
| `panel` | string | HTML entry point (e.g., `index.html`) |
| `icon` | string | Icon file path (PNG) |
| `panelWidth` | number | Default width in pixels (default: 380) |
| `panelMaxHeight` | number | Max height in pixels (default: 560) |
| `events` | array | Declared events: `excerpt`, `comments`, `practice`, `synced`, `undo`, `imported` |
| `cardCommands` | array | Card menu commands with `id`, `title`, `when` conditions |
| `excerptClaim` | object | Filter excerpts by hashtag; includes `hashtag` and optional `configKey` |

### Core API patterns

```javascript
// GET request
const state = await mn.api('/ui-state');
const topicid = state.state.topicid;

// POST request
await mn.api('/notes/batch-get', {
  method: 'POST',
  body: { noteIds: [noteid], addons: ['*'] }
});

// Always check for error field (no exception thrown)
const r = await mn.api('/some/path');
if (r.error) {
  console.log(r.error.code, r.error.message);
} else {
  // Process r
}
```

### Event listening

```javascript
// Declare in mnaddon.json
"events": ["excerpt", "comments", "cardCommand"]

// Listen in panel
mn.on('excerpt', async (d) => {
  const { noteid, opened } = d;
  // Process excerpt
});

mn.on('cardCommand', (d) => {
  const { id, noteID } = d;
  // Handle command
});
```

### Template.json structure (minimal)

```json
{
  "id": "u-my-template",
  "name": "My Template",
  "plugins": ["com.example.my-addon"],
  "pluginConfigs": {
    "com.example.my-addon": { "key": "value" }
  },
  "bottom": {
    "modes": [{
      "id": "collect",
      "behavior": "write",
      "tools": ["tool.excerptBox", "addon.com.example.my-addon.primary"]
    }]
  },
  "selectionMenu": {
    "cards": ["addon.com.example.my-addon.mycommand:"]
  }
}
```

### Panel UI control

```javascript
mn.fit()              // Auto-fit height to content
mn.height(h)          // Set height
mn.size(w, h)         // Set width and height (animated)
mn.mode('mini')       // Switch to mini mode
mn.close()            // Close panel
mn.currentMode        // Read current mode: 'workspace' or 'mini'
```

### Read template config at runtime

```javascript
const r = await mn.api('/immersive/template');
if (r && r.available) {
  const plugins = r.plugins || [];
  const myPlugin = plugins.find(p => p.addon === 'com.example.my-addon');
  if (myPlugin && myPlugin.allowed) {
    console.log(myPlugin.config);
  }
}
```

## Decision guidance

| Scenario | Use | Why |
|----------|-----|-----|
| Building first add-on | MN Rails scaffold | Follows conventions, includes build tools, templates |
| Simple read-only panel | Panel-only project | No native code needed |
| Need native JS access | Hybrid project | Includes `main.js` and panel bridge |
| Filtering excerpts | `excerptClaim` in mnaddon.json | Reduces noise, auto-opens panel only for matching tags |
| Conditional menu items | `when` conditions in `cardCommands` | Hides commands based on card state, hashtags, add-on fields |
| Sharing config across templates | `pluginConfigs` in template.json | Template controls plugin behavior without code changes |
| Detecting mode changes | CSS `data-mode` attribute or `MutationObserver` | Simpler than polling `mn.currentMode` |
| Non-JSON responses | Use `fetch()` directly | `mn.api()` always parses as JSON; use `fetch()` for images, markdown, binary |

## Workflow

### Building a new add-on

1. **Create project**: Run `npm create mn-rails@latest`, choose framework and kind (panel or hybrid).
2. **Define manifest**: Edit `mnaddon.json` with unique `addonid`, version, events, and commands.
3. **Build panel**: Write `src/App.tsx` (or Vue/Svelte equivalent) to render the UI.
4. **Declare events**: List events in `mnaddon.json` `events` array; listen with `mn.on()` in panel code.
5. **Call APIs**: Use `mn.api()` to read/write card data, query state, trigger actions.
6. **Handle errors**: Check for `error` field in every response; do not rely on exceptions.
7. **Build and test**: Run `npm run build`, then `npm run sync` (macOS) or manually copy to MarginNote add-ons folder.
8. **Package**: Run `npm run package` to create `.mnaddon` installer.

### Integrating add-on with template

1. **Create template.json**: Start with minimal `id` and `name`.
2. **Whitelist add-on**: Add `addonid` to `plugins` array.
3. **Pass config**: Add entry to `pluginConfigs` with add-on id as key.
4. **Add toolbar button**: Include `addon.<id>.primary` in `bottom.modes[].tools`.
5. **Add menu commands**: Declare `cardCommands` in mnaddon.json, then add `addon.<id>.<cmd>:` to `selectionMenu.cards` (note trailing colon).
6. **Verify at runtime**: Call `GET /immersive/template` to confirm whitelist and config reached panel.
7. **Package template**: Create `.mntemplate` with `template.json`, `manifest.json`, `localizations.json`, and `plugins/` folder.

### Handling card data

1. **Get current context**: Call `GET /ui-state` to read `topicid` and `focusnoteid`.
2. **Fetch card details**: POST to `/notes/batch-get` with `noteIds` and optional `addons` array.
3. **Check for errors**: Verify `found` field and `error` field in response.
4. **Modify card**: POST to `/notes/{noteid}/edits` with operations array (setTitle, appendComment, setAddonValues, etc.).
5. **Confirm writes**: Include `confirm: true` in POST body.
6. **Verify persistence**: Check `persisted` field in response.

## Common gotchas

- **API errors don't throw**: `mn.api()` returns error objects as normal responses. Always check `if (r.error)` before using response data.
- **Menu items require both sides**: Command must be declared in `mnaddon.json` `cardCommands` AND added to template `selectionMenu`. Missing either side = no menu item.
- **Typos in template silently fail**: Misspelled field names, wrong `addon.` prefix format, or missing trailing colons in menu items don't error—they just don't appear. Use `GET /immersive/template` to verify.
- **Panel not declared in events**: Listeners for undeclared events have no effect. Always declare in `mnaddon.json` `events` array first.
- **Excerpt events only fire if panel is listening**: If `excerptClaim` is set, only matching excerpts trigger the event. If `excerptClaim` is omitted, all excerpts open the panel.
- **Don't place interactive UI in top 36px**: Every panel has a 36px drag bar at the top. Read `--mn-drag-h` CSS variable or avoid that area.
- **Duplicate events within 1.5s are dropped**: Rapid-fire events for the same card are deduplicated.
- **Template config is not auto-saved**: Changes to `template.json` require re-importing the template package.
- **`mn.mode()` doesn't resize**: Calling `mn.mode('mini')` only flips the flag. Call `mn.size()` or `mn.fit()` to actually resize.
- **Hybrid projects must not have root main.js in panel-only builds**: Panel-only projects should not include `native/main.js` in the build output.
- **Hashtag routes required for practice verdicts**: Writing practice verdicts requires the template to declare a hashtag prefix and error reason list in `settings.practice`.

## Verification checklist

Before submitting an add-on or template:

- [ ] `mnaddon.json` has unique `addonid`, valid `marginnote_version_min`, and `immersiveOnly: true` (for scene add-ons).
- [ ] All declared `events` are actually listened to in panel code with `mn.on()`.
- [ ] All `cardCommands` are added to template `selectionMenu` with trailing colons (e.g., `addon.id.cmd:`).
- [ ] `plugins` array in template.json includes the add-on's `addonid`.
- [ ] `pluginConfigs` has an entry for each plugin in the whitelist.
- [ ] Panel code checks for `error` field in every `mn.api()` response.
- [ ] `template.json` has required `id` and `name` fields.
- [ ] `manifest.json` lists all plugins with correct `pluginID`, `folder`, `version`, and `title`.
- [ ] `localizations.json` provides translations for template name.
- [ ] Build passes: `npm run build` completes without errors.
- [ ] Project passes doctor check: `npm run doctor` reports no issues.
- [ ] Panel adapts to both modes: test `workspace` and `mini` layouts.
- [ ] Verify at runtime: call `GET /immersive/template` and confirm `allowed: true` and correct `config`.
- [ ] Test on target MarginNote version (check `marginnote_version_min`).

## Resources

- **Comprehensive page listing**: [dev.marginnote.com.cn/llms.txt](https://dev.marginnote.com.cn/llms.txt) — full navigation of all documentation pages.
- **Getting started**: [Introduction](https://dev.marginnote.com.cn/en/introduction) — overview of scene add-ons and templates.
- **API reference**: [Capabilities endpoint](https://dev.marginnote.com.cn/api-reference/能力与状态/获取能力清单) — query available routes, limits, and enums.
- **MN Rails**: [Creating an add-on project](https://dev.marginnote.com.cn/en/mnrails) — scaffold, build, and deploy workflow.

---

> For additional documentation and navigation, see: https://dev.marginnote.com.cn/llms.txt