Ini adalah call pembuatan yang didapat koneksi Claude untuk Notes di osFoundry, diambil lewat tools/list tanpa autentikasi ke endpoint MCP publik pada 2026-11-09, lalu dipangkas:
{"name": "create_note_tab",
"description": "Create a new Slides or Sheet tab in an approved Note. Use the version from list_note_tabs; returns the exact new tab ID for later edits. An optional outline fills a Slides tab. A repeated idempotency key returns the first tab.",
"inputSchema": {
"required": ["note_id", "expected_version", "idempotency_key", "tab_type"],
"properties": {
"tab_type": {"enum": ["slides", "sheet"]},
"label": {"type": "string"},
"slides": {"type": "array", "minItems": 1, "maxItems": 20,
"items": {"required": ["title"], "additionalProperties": false,
"properties": {"title": {}, "body": {}, "notes": {}}}}},
"additionalProperties": false},
"annotations": {"readOnlyHint": false, "destructiveHint": true},
"securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:write"]}]}
Dua tool tetangganya untuk pekerjaan ini:
{"name": "add_note_source",
"description": "Add an HTTP or HTTPS citation to the Sources tab of an approved Note. Uses the Note version and an idempotency key; returns the new source ID.",
"inputSchema": {"required": ["note_id", "expected_version", "idempotency_key", "title", "url"]}}
{"name": "list_note_tabs",
"description": "List every tab instance and its type. A Note may have multiple Slides or Sheet tabs; use the exact tab ID returned here."}
Ada tiga frasa yang menopang desainnya: "every tab instance", "returns the exact new tab ID", dan "a repeated idempotency key returns the first". Sisa tulisan ini membangun ulang tool-tool itu di Python dan mengukur apa yang dicegah oleh masing-masing frasa. Optimistic locking pada versi Note dibahas di tulisan lain dan di sini hanya muncul di bagian yang bersinggungan dengan dua hal lainnya.
Apa sebenarnya yang bisa Claude taruh di sebuah slide?
Lebih sedikit daripada asisten bawaan aplikasi desktop. Di dalam osFoundry, slide_create_deck_from_outline dan slide_add_slide milik asisten Note (di apps/web/src/screens/notes/lib/note-surface-tools.ts) menerima layout, bullet, konten dua kolom, tabel, gambar, dan salah satu dari lima tema. Item outline milik connector hanya title, body, notes, dengan additionalProperties: false. edit_note_tab menyediakan slide_add untuk satu slide dan slides_add_outline untuk sebuah array, keduanya dengan tiga field yang sama.
code/note_store.py memuat respons tools/list yang disimpan dan memeriksa payload terhadapnya dengan validator kecil (required, enum, additionalProperties, minItems/maxItems). Delapan payload yang mungkin saja dikirim sebuah model:
edit_note_tab {"operation": "slide_add", "title": "Sources", "body": "3 links"} -> ok
edit_note_tab {"operation": "slide_add", "title": "Sources", "index": 0} -> unexpected index
edit_note_tab {"operation": "slides_add", "slides": [{"title": "a"}]} -> operation: not in enum
edit_note_tab {"operation": "slides_add_outline", "slides": [{... "bullets"}]} -> slides: [0] unexpected bullets
create_note_tab {"tab_type": "slides", "slides": "[21 items]"} -> slides: maxItems 20
create_note_tab {"tab_type": "markdown"} -> tab_type: not in enum
add_note_source {"url": "https://example.org/report"} -> missing title
list_note_attachments {"kind": "sources", "limit": 100} -> ok
Yang terakhir lolos validator ini, tapi tidak lolos schema live secara penuh: limit punya maximum: 50, dan rebuild ini tidak memeriksanya. slide_add tidak bisa memilih posisi, create_note_tab tidak bisa membuat tab Markdown, dan deck yang lebih dari 20 slide butuh lebih dari satu call. Schema yang sempit adalah pilihan soal siapa yang menata deck: connector menulis konten, orang di aplikasi desktop yang menerapkan tema dan layout.
Kenapa harus tab ID yang persis, bukan "tab slides"?
Karena aplikasi desktop sendiri masih menerima jalan pintas itu, dan jalan pintas itu memilih kecocokan pertama. resolveTabInstanceId() di apps/web/src/screens/notes/types.ts:
if (cfg.some((t) => t.id === rawTabId)) return rawTabId
const byType = cfg.find((t) => t.type === rawTabId)
if (byType) return byType.id
Fallback itu menjaga link lama seperti …/slides tetap jalan. Bagi agent, artinya "edit slides" akan masuk ke tab Slides mana pun yang ada paling depan di strip. Tab baru mendapat ID seperti slides-9-mh8z1k2a dari nextTabId() di NotesContentPanel.tsx, tab tetapnya adalah markdown, files, dan sources, dan seseorang bisa menambah tab Slides sebanyak apa pun, menggesernya ke urutan baru, dan mengganti namanya, termasuk nama duplikat (handleRename hanya melakukan trim).
Rebuild ini memberi sebuah Note K tab Slides dengan label berbeda. Agent yang di-script me-list tab, memilih satu deck sebagai target, dan menyimpan pegangan ke deck itu: posisinya di strip, tipenya, labelnya, atau ID-nya. Di antara giliran agent, orang itu membuat nol sampai dua perubahan acak (memindahkan tab 40%, mengganti nama tab Slides ke salah satu dari lima label 35%, menambah tab Slides 25%). Lalu agent me-list lagi dan me-resolve pegangannya. 10,000 percobaan per K, seed 7:
| K deck | pegangan | benar | deck salah | tidak ditemukan |
|---|---|---|---|---|
| 2 | posisi | 83.7% | 16.3% | 0.0% |
| 2 | tipe | 50.7% | 49.3% | 0.0% |
| 2 | label | 85.6% | 2.1% | 12.3% |
| 2 | ID | 100% | 0 | 0 |
| 5 | posisi | 82.7% | 17.3% | 0.0% |
| 5 | tipe | 19.7% | 80.3% | 0.0% |
| 5 | label | 92.1% | 3.0% | 5.0% |
| 5 | ID | 100% | 0 | 0 |
Pegangan berdasarkan tipe benar 1/K kali, sesuai perkiraan. Posisi gagal setiap kali ada geseran yang melintasi target, berapa pun K-nya. Label adalah yang paling menggoda karena itulah yang diketik orang di prompt, dan hasilnya benar hampir setiap saat. 2–3% saat ia salah adalah kasus yang buruk: deck lain sekarang memakai label itu, dan agent menulis ke dalamnya tanpa error apa pun. "Tidak ditemukan" adalah kegagalan yang aman. ID diberikan sekali dan tidak pernah dipakai ulang, jadi ia bertahan dari semua perubahan yang bisa dilakukan orang di strip tab.
Bukankah pengecekan versi sudah menangkap tab yang salah?
Tidak. Setiap perubahan strip tab di atas menaikkan versi Note, jadi write yang basi ditolak dan agent me-list lagi. List ulang itu memberinya versi terkini, lalu ia me-resolve pegangan lamanya terhadap list baru. Di rebuild ini, 100% write tersebut lolos pengecekan versi, termasuk semua yang mengarah ke deck yang salah. Versi membuktikan agent sudah melihat Note terbaru. Versi tidak bilang apa-apa soal apakah agent menemukan tab yang benar di dalamnya. Bagian itu urusan tab ID.
Di mana pengecekan idempotensi diletakkan, sebelum atau sesudah pengecekan versi?
Sebelum, kalau tidak janji di deskripsi tool akan rusak. create_note_tab dijalankan 50 kali per run dengan 30% respons hilang setelah server melakukan commit, dan itulah wujud timeout host dari sisi client. Client mengirim ulang call yang sama. Tiga kebijakan client, dua urutan server, seed 1–3:
| server memeriksa | kebijakan key client | tab yang dibuat (call yang dikirim) |
|---|---|---|
| key, lalu versi | key baru per call | 63 (76) / 75 (100) / 62 (74) |
| key, lalu versi | key sama saat retry | 50 (67) / 50 (81) / 50 (62) |
| key, lalu versi | key baru setelah conflict | 50 (67) / 50 (81) / 50 (62) |
| versi, lalu key | key baru per call | 63 (76) / 75 (100) / 62 (74) |
| versi, lalu key | key sama saat retry | 50 (79) / 50 (98) / 50 (72) |
| versi, lalu key | key baru setelah conflict | 72 (94) / 73 (96) / 61 (72) |
Kiriman ulang membawa versi dari sebelum commit yang hilang. Server yang memeriksa versi lebih dulu melihat versi basi dan mengembalikan conflict, bukan hasil pertama. Client yang merespons conflict dengan cara yang masuk akal, yaitu membaca ulang dan memperlakukan percobaan berikutnya sebagai intent baru dengan key baru, sekarang membuat deck kedua. Bahkan client yang mempertahankan key-nya mengirim 16–21% call lebih banyak. Kalau key diperiksa lebih dulu, kiriman ulang mengembalikan tab ID asli, apa pun kata versinya. Itulah arti yang seharusnya dari "a repeated idempotency key returns the first tab".
Key seperti apa yang cocok untuk add_note_source?
Rebuild ini menjalankan 300 sesi riset. Masing-masing punya tiga giliran, setiap giliran mengutip 6 dari 8 halaman, dan sebuah halaman bisa kembali dalam salah satu dari tujuh penulisan: dengan slash di akhir, www., ?utm_source=…, #fragment, http://, atau tanpa scheme. 20% respons hilang dan dikirim ulang dengan key yang sama.
| key | source tersimpan | duplikat | per sesi |
|---|---|---|---|
| UUID baru per sitasi, dipakai ulang saat kirim ulang | 5,400 | 3,038 | 10.13 |
| hash URL apa adanya | 4,814 | 2,452 | 8.17 |
| hash URL yang dinormalisasi dan Note ID | 2,362 | 0 | 0.00 |
Key baru per sitasi menangani respons yang hilang (tidak ada entri di baris pertama itu yang berasal dari kiriman ulang) dan tidak lebih dari itu. Riset mengutip halaman yang sama lagi di giliran berikutnya, dan setiap giliran baru berarti key baru. Hash URL apa adanya menangkap pengulangan yang persis sama, tapi tidak menangkap ?utm_source= atau slash di akhir. Key yang menghilangkan duplikat adalah key yang menamai intent-nya, "halaman ini adalah source dari Note ini", dengan meng-hash note_id ditambah URL yang dinormalisasi (https, host huruf kecil tanpa www., tanpa fragment, tanpa utm_*, tanpa slash di akhir).
Form Sources milik aplikasi desktop melakukan versi yang lebih ringan dari ini. NotesSourcesTab.tsx menormalisasi link yang diketik dengan new URL(...) dan menolak link yang sudah terlampir ("This source is already attached"). Di rebuild ini, membandingkan href ala browser akan menandai 725 dari 3,038 duplikat di baris pertama. add_note_source milik connector tidak menyebutkan adanya pengecekan URL, jadi key-lah tempat agent menyatakannya. Key yang diturunkan dari URL punya satu efek samping: menambahkan ulang halaman yang sama dengan deskripsi yang lebih baik akan mengembalikan baris pertama. Untuk mengeditnya, gunakan tab Sources.
Satu kali create dengan outline, atau satu slide per call?
Biaya deck 12 slide, dengan setiap request divalidasi terhadap schema live:
| pendekatan | write | byte request | conflict per deck |
|---|---|---|---|
create_note_tab dengan slides
|
1 | 1,673 | 0.070 |
create kosong + 12 × slide_add
|
13 | 5,237 | 0.817 |
Kolom conflict adalah model, bukan hasil pengukuran terhadap osFoundry: 4 s antara read dan write agent, seseorang menyimpan di suatu tempat di Note rata-rata sekali per menit (eksponensial), 2,000 deck yang disimulasikan. Setiap write adalah jendela baru tempat versi tingkat Note bisa berubah, jadi membangun per slide butuh read ulang sekitar dua belas kali lebih banyak. Masukkan deck ke outline (sampai 20 slide) dan pakai slide_add untuk slide yang baru kamu ketahui belakangan.
Paging adalah angka lain yang perlu diketahui. list_note_attachments membatasi limit di 50, jadi Note dengan 120 source butuh 3 call untuk di-list.
Cara rebuild ini dijalankan
Tidak ada binary Claude yang berjalan di mesin ini. code/run.py berperan sebagai agent dan memanggil code/note_store.py secara langsung. Store-nya ada di memori di balik satu lock, dengan counter sebagai versi. Nama tool, field wajib, enum, dan batas item diambil dari schema live di code/tools_list.raw, dan tab ID mengikuti nextTabId(). Bentuk error, cakupan key (di sini per Note), dan urutan idempotensi adalah pilihan rebuild ini sendiri, dan urutan kedua ada hanya untuk menunjukkan apa yang salah. Python 3.9.6 di Apple Silicon, kelima eksperimen selesai dalam 1.0 s wall time. Output-nya ada di code/run.out.
Kalau kamu memasangnya sendiri
- Kembalikan ID per instance dari setiap call list dan create, dan jadikan wajib di tool edit. Jangan me-resolve tipe atau label di jalur write, walaupun UI kamu masih menerimanya di URL.
- Periksa idempotency key sebelum versi. Kiriman ulang setelah respons hilang selalu membawa versi lama.
- Jelaskan ke agent di deskripsi tool apa yang harus dinamai oleh sebuah key. Untuk sitasi, "URL ini di dokumen ini", yang dinormalisasi, menghilangkan pengulangan antar giliran. UUID acak hanya menangani kiriman ulang.
- Buat bentuk slide di connector tetap kecil (title, body, notes) dan serahkan desain ke orang di editor. Schema kecil berarti lebih sedikit field yang bisa salah diisi model.
- Biarkan agent mengirim satu outline utuh dalam satu call dan beri batas (osFoundry memakai 20). Lebih sedikit write berarti lebih sedikit jendela conflict di dokumen bersama.
- Kembalikan metadata lampiran, jangan pernah konten atau signed link, lewat kanal agent. Sebuah link hidup lebih lama daripada sesi yang mengambilnya.
Apa yang disimpan tab Sources di sisi pengguna ada di halaman tab Sources. Protokol yang mendasarinya dirangkum di entri glosarium MCP osFoundry.
Top comments (0)