Two tools for the model, note_add and note_list, and /notes for the person, all backed by the plugin's own store so the notes outlive the session. Needs…

モデルに道具を 2 つ、人にスラッシュコマンドを 1 つ足す Mod。中身は $.store。
| 名前 | 誰が使う | 何をする |
|---|---|---|
mcp__notes-tool__note_add | モデル | { text } を 1 件保存する |
mcp__notes-tool__note_list | モデル | 保存済みのメモを番号付きで返す |
/notes | 人 | 保存済みのメモを保存時刻つきで表示する |
$.tool.register で登録した道具の正式名は mcp__<plugin>__<name>。つまり プラグイン名 notes-tool なので mcp__notes-tool__note_add になる。
claude --plugin-dir C:/Users/lifes/kasegu/mods/notes-tool
グローバル設定の env に CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 が必要。
$.store は Claude Code の設定ディレクトリ配下にある、この Mod 専用の JSON ファイル。
C:\Users\lifes\.claude\plugins\store\notes-tool_inline-0716ddbdc635.json
セッションをまたいで残る(claude -p を 2 回に分けても note_list は前回のメモを返す)。
tool.call フックが next を呼ばずに { result: ... } を返すと、そのフック自身が 道具の本体になる。拒否したいときは { deny: '<理由>' }。{ text } や { content } ではない。詳細は NOTES.md。
claude plugin validate .
claude plugin test .hooks/register.ts 63 lines1import type { Register } from 'claude-code'
2
3// The store is this plugin's own JSON file under the user's Claude Code
4// configuration directory, so a note written in one session is read in the
5// next one. Nothing else in the session can reach it.
6const KEY = 'notes'
7
8type Note = { text: string; at: string }
9
10const read = (raw: unknown): Note[] => (Array.isArray(raw) ? (raw as Note[]) : [])
11
12export const register: Register = on => {
13 on('session.start', async ($, e, next) => {
14 const r = await next(e)
15 await $.tool.register({
16 name: 'note_add',
17 description: 'Save a short note for later in this project. Input: { text }',
18 inputSchema: {
19 type: 'object',
20 properties: { text: { type: 'string' } },
21 required: ['text'],
22 },
23 })
24 await $.tool.register({
25 name: 'note_list',
26 description: 'List every note saved so far in this project. No input.',
27 inputSchema: { type: 'object', properties: {} },
28 })
29 await $.command.register({
30 name: 'notes',
31 description: 'List saved notes (notes-tool)',
32 })
33 return r
34 })
35
36 // A tool.call hook that answers without `next` is the tool: core validates
37 // the answer, maps it for the model and records it as the tool's result.
38 on('tool.call', { tool: 'mcp__notes-tool__note_add' }, async ($, e) => {
39 const text = String(e.text ?? '').trim()
40 if (text === '') return { deny: 'note_add needs a non-empty text' }
41 const notes = read(await $.store.get(KEY))
42 notes.push({ text, at: new Date().toISOString() })
43 await $.store.set(KEY, notes)
44 return { result: `saved note ${notes.length}: ${text}` }
45 })
46
47 on('tool.call', { tool: 'mcp__notes-tool__note_list' }, async ($, e) => {
48 const notes = read(await $.store.get(KEY))
49 if (notes.length === 0) return { result: 'no notes saved yet' }
50 return { result: notes.map((n, i) => `${i + 1}. ${n.text}`).join('\n') }
51 })
52
53 on('command.run', { command: 'notes' }, async ($, e) => {
54 const notes = read(await $.store.get(KEY))
55 return {
56 text:
57 notes.length === 0
58 ? 'notes-tool: nothing saved yet'
59 : notes.map((n, i) => `${i + 1}. ${n.text} (${n.at})`).join('\n'),
60 }
61 })
62}
63