Zernote
User research workspace to transcribe interviews and turn conversations into insights.
사용해야 할까요
품질 및 안전성
발견 사항 (1)
- LOWget-skills에서
도구 정의와 프로토콜 준수에 대한 자동 분석을 기반으로 합니다.
컨텍스트 비용
이는 서버의 도구가 모델의 컨텍스트에 로드될 때마다 소비되는 대략적인 토큰 수입니다. 수치가 높을수록 다른 작업에 사용할 수 있는 주의가 줄어듭니다.
설치
원클릭 설치
`claude_desktop_config.json` 파일에 다음을 추가하세요:
{
"mcpServers": {
"zernote": {
"url": "https://zernote.com/mcp"
}
}
}원격 엔드포인트
https://zernote.com/mcpstreamable-http할 수 있는 일
도구 목록
도구 (14)
🟢get-skills(id)
Use for startup ideas, office hours, and “I installed this connector to work through an idea”. Inside: office-hours-zernote (challenge the idea), zernote-plan-research. Omit id for the catalog, then pass id for the full playbook. Not for listing interviews or projects. Works without login.
입력 스키마
{
"type": "object",
"properties": {
"id": {
"description": "Skill id. Empty = catalog. Examples: office-hours-zernote, zernote-plan-research.",
"type": "string"
}
}
}🟢projects_read(action, limit, offset)
Read existing research folders. Not for a new startup idea or office hours — call get-skills first. action=list returns projects for the authenticated user.
입력 스키마
{
"type": "object",
"properties": {
"action": {
"description": "Read action. Currently only list.",
"enum": [
"list"
],
"type": "string"
},
"limit": {
"description": "Max number of projects to return (default 50, max 200).",
"type": "integer"
},
"offset": {
"description": "Number of projects to skip before returning results (default 0).",
"type": "integer"
}
},
"required": [
"action"
]
}🔴projects_edit(action, description, id, name)
Create or rename projects. action=create needs name; action=rename needs id and name. Delete is not supported yet.
입력 스키마
{
"type": "object",
"properties": {
"action": {
"description": "Edit action.",
"enum": [
"create",
"rename"
],
"type": "string"
},
"description": {
"description": "Optional project description (create/rename).",
"type": "string"
},
"id": {
"description": "Project UUID (required for rename).",
"type": "string"
},
"name": {
"description": "Project name (required for create and rename).",
"type": "string"
}
},
"required": [
"action"
]
}🟢interviews_read(action, around_chunk_index, folder_id, id, inbox_only, ...)
Read interviews. Typical path: (1) action=list with summary_mode=brief (default) — returns the short # Summary. List items include need_approve_speakers (+ pending_speakers / hint): when true, show interviewer quote links then confirm via assign_speakers; one identity confirm backfills other interviews sharing voice_identity_id. Use summary_mode=brief for renaming or folder triage. Use summary_mode=full when Profile/Tools/Pains/Other topics are needed for deeper analysis. (2) call `search` when you need evidence or counter-evidence for a hypothesis; (3) use action=transcript with around_chunk_index from matching_chunks when a quote window is required. CITATION REQUIRED: when answering the user, actively cite evidence as markdown ["verbatim quote"]({base_url}/project/interview/{id}?t={seconds}) using interview_url / matching chunk url from tool results (includes ?t= when start_seconds is set). Prefer quotes over paraphrase.
입력 스키마
{
"type": "object",
"properties": {
"action": {
"description": "Read action. Use list (brief or summary_mode=full) for overview; use transcript when you need a quote window.",
"enum": [
"list",
"get",
"status",
"transcript"
],
"type": "string"
},
"around_chunk_index": {
"description": "Transcript only: expand a window around this vector-search chunk_index (use instead of blind offset when available).",
"type": "integer"
},
"folder_id": {
"description": "Deprecated alias for project_id.",
"type": "string"
},
"id": {
"description": "Interview UUID (required for get, status, transcript).",
"type": "string"
},
"inbox_only": {
"description": "When true with action=list, list Inbox interviews (no project). Ignored if project_id is set.",
"type": "boolean"
},
"limit": {
"description": "For list: max interviews (default 20, max 100). For transcript: max characters (default 4000, max 20000).",
"type": "integer"
},
"notes_mode": {
"description": "List only. none (default) = omit notes body. full = researcher notes (truncated on list). Use summary when notes are not required.",
"enum": [
"none",
"full"
],
"type": "string"
},
"offset": {
"description": "For list: skip count. For transcript without around_chunk_index: character offset (default 0).",
"type": "integer"
},
"project_id": {
"description": "Optional project UUID to filter list (alias: folder_id).",
"type": "string"
},
"radius_chars": {
"description": "Transcript only with around_chunk_index: chars before/after chunk match (default 1500, max 8000).",
"type": "integer"
},
"summary_mode": {
"description": "List only. brief (default) = short # Summary for triage/rename. full = all summary_fragments (profile/tools/pains/other_topics) when brief is insufficient.",
"enum": [
"brief",
"full"
],
"type": "string"
}
},
"required": [
"action"
]
}🔴interviews_edit(action, assigned_names, enabled, folder_id, id, ...)
Rename, move, write summary/notes, retranscribe, assign/confirm speakers, or toggle voiceprint. action=rename needs id+title; move needs interview_ids (+ optional project_id); write_summary needs id+summary; write_notes needs id+notes; retranscribe needs id+language (charges transcription tokens again; analyze-only, keeps notes). assign_speakers needs id+speaker_labels+assigned_names. For voiceprint interviewer confirm: one label + real name on one interview after quote links; that identity backfills other interviews sharing voice_identity_id — re-list instead of confirming every card. set_voiceprint needs enabled bool (false purges biometric voice data). If need_approve_speakers=true, show quote links then assign_speakers when the user confirms. If transcript_quality.looks_corrupted=true, offer retranscribe with the correct language. write_notes and write_summary replace the entire notes or summary field (not append).
입력 스키마
{
"type": "object",
"properties": {
"action": {
"description": "Edit action.",
"enum": [
"rename",
"move",
"write_summary",
"write_notes",
"retranscribe",
"assign_speakers",
"set_voiceprint"
],
"type": "string"
},
"assigned_names": {
"description": "Human names matching speaker_labels (assign_speakers). Use a real name, not \"Interviewer N\".",
"items": {
"type": "string"
},
"type": "array"
},
"enabled": {
"description": "Voiceprint on/off (set_voiceprint). false deletes stored voice biometrics.",
"type": "boolean"
},
"folder_id": {
"description": "Deprecated alias for project_id.",
"type": "string"
},
"id": {
"description": "Interview UUID (rename, write_summary, write_notes, retranscribe, assign_speakers).",
"type": "string"
},
"interview_ids": {
"description": "Interview UUIDs to move.",
"items": {
"type": "string"
},
"type": "array"
},
"language": {
"description": "Transcription language code for retranscribe (e.g. en, ru, nl). Charges tokens for audio duration.",
"type": "string"
},
"notes": {
"description": "Full note text that replaces the existing notes field (write_notes).",
"type": "string"
},
"project_id": {
"description": "Destination project UUID for move (alias: folder_id). Omit or null for Inbox.",
"type": "string"
},
"speaker_labels": {
"description": "Diarization labels to assign (assign_speakers), e.g. [\"A\"]. Voiceprint interviewer confirm: one label in this call (same identity then backfills other interviews).",
"items": {
"type": "string"
},
"type": "array"
},
"summary": {
"description": "Full summary markdown that replaces the existing summary (write_summary). Expected sections: # Summary, ## Profile, ## Tools & workflow, ## Pains & friction, # Other topics.",
"type": "string"
},
"title": {
"description": "New title (rename).",
"type": "string"
}
},
"required": [
"action"
]
}🟢search(chunks_limit, chunks_offset, interview_id, limit, query)
Semantic search for evidence / counter-evidence after you have interview context (e.g. from list/summary). Optional interview_id scopes search to one interview and returns smaller text windows. Returns matching_chunks[{chunk_index, text, similarity, start_seconds?, url}]. CITATION REQUIRED: actively cite hits to the user as markdown ["verbatim quote from text"]({base_url}/project/interview/{id}?t={seconds}) using each chunk's url field from the tool result (already absolute; includes ?t= when start_seconds is present). Prefer quoting matching_chunks over paraphrase. Expand with interviews_read action=transcript around_chunk_index when a wider quote window is needed.
입력 스키마
{
"type": "object",
"properties": {
"chunks_limit": {
"description": "Max matching chunks per interview (default 3 corpus / 8 when interview_id set; max 10 / 20).",
"type": "integer"
},
"chunks_offset": {
"description": "Skip this many top chunks per interview (default 0).",
"type": "integer"
},
"interview_id": {
"description": "Optional interview UUID — search only inside this interview; returns smaller chunk windows.",
"type": "string"
},
"limit": {
"description": "Max number of interviews to return (default 5, max 10). Ignored when interview_id is set.",
"type": "integer"
},
"query": {
"description": "Natural language search query (hypothesis / claim to evidence).",
"type": "string"
}
},
"required": [
"query"
]
}🟢cards_read(codes, folder_id, interview_id, interview_ids, lens, ...)
List L2 coded claim-cards (claim + optional quote + chunk_index + lens + codes + start_seconds? + url). Filter by project_id, interview_id / interview_ids, codes (any match), or lens (workflow|gtm|privacy|other). CITATION REQUIRED: actively cite each card.quote to the user as markdown ["verbatim quote"]({base_url}/project/interview/{id}?t={seconds}) using item.url from the tool result (already absolute; includes ?t= when start_seconds is set). Prefer quotes over paraphrase. Verify with interviews_read action=transcript around_chunk_index when a wider quote window is needed.
입력 스키마
{
"type": "object",
"properties": {
"codes": {
"description": "Optional code tags; any match (e.g. pain:synthesis, gtm:pricing).",
"items": {
"type": "string"
},
"type": "array"
},
"folder_id": {
"description": "Deprecated alias for project_id.",
"type": "string"
},
"interview_id": {
"description": "Optional interview UUID to scope cards.",
"type": "string"
},
"interview_ids": {
"description": "Optional interview UUIDs to scope cards.",
"items": {
"type": "string"
},
"type": "array"
},
"lens": {
"description": "Optional exact lens filter.",
"enum": [
"workflow",
"gtm",
"privacy",
"other"
],
"type": "string"
},
"limit": {
"description": "Max cards to return (default 50, max 200).",
"type": "integer"
},
"offset": {
"description": "Number of cards to skip (default 0).",
"type": "integer"
},
"project_id": {
"description": "Optional project UUID — cards from interviews in this project (alias: folder_id).",
"type": "string"
}
}
}🟢insights_read(action, folder_id, group_id, group_name, id, ...)
Read the project Insights graph in pages or by node. action=list returns a compact folder-wide outline (groups + interviews) plus a page of insight bodies. Filter with interview_id, group_id (DB UUID, not UI group-0), group_name, insight_ids, or ungrouped_only. action=get_node returns one node: insight | interview | group | ungrouped. CITATION REQUIRED: actively cite each item.quote as markdown ["verbatim quote"]({base_url}/project/interview/{id}?t={seconds}) using item.url from the tool result (already absolute; includes ?t= when start_seconds is set). Prefer quotes over paraphrase. Do not use quote_start — that is a character offset, not a player timestamp.
입력 스키마
{
"type": "object",
"properties": {
"action": {
"description": "list = paged insight bodies + outline. get_node = one insight, interview, group, or ungrouped cluster.",
"enum": [
"list",
"get_node"
],
"type": "string"
},
"folder_id": {
"description": "Deprecated alias for project_id.",
"type": "string"
},
"group_id": {
"description": "List filter: insight_groups.id UUID (not a render index like group-0).",
"type": "string"
},
"group_name": {
"description": "List filter: case-insensitive group name match.",
"type": "string"
},
"id": {
"description": "get_node: insight, interview, or group UUID. Not used for ungrouped.",
"type": "string"
},
"insight_ids": {
"description": "List filter: only these insight UUIDs.",
"items": {
"type": "string"
},
"type": "array"
},
"interview_id": {
"description": "List filter: only insights from this interview. Interview must be in the project.",
"type": "string"
},
"limit": {
"description": "List only: max insight bodies (default 25, max 100).",
"type": "integer"
},
"node_type": {
"description": "get_node: which node to load.",
"enum": [
"insight",
"interview",
"group",
"ungrouped"
],
"type": "string"
},
"offset": {
"description": "List only: skip this many matching insight bodies (default 0).",
"type": "integer"
},
"project_id": {
"description": "Project/folder UUID (required for list and for get_node group/ungrouped/insight).",
"type": "string"
},
"ungrouped_only": {
"description": "List filter: only insights that are not in a cross-interview group.",
"type": "boolean"
}
},
"required": [
"action"
]
}🟢jtbd_read(action, folder_id, interview_id, project_id, segment_id)
Read JTBD job graph segments and trees. action=list_segments needs folder_id/project_id. action=get_tree needs segment_id or interview_id. CITATION REQUIRED: cite job quotes as ["text"](interview_url).
입력 스키마
{
"type": "object",
"properties": {
"action": {
"enum": [
"list_segments",
"get_tree"
],
"type": "string"
},
"folder_id": {
"description": "Project/folder UUID (list_segments).",
"type": "string"
},
"interview_id": {
"description": "Interview UUID (get_tree).",
"type": "string"
},
"project_id": {
"description": "Alias for folder_id.",
"type": "string"
},
"segment_id": {
"description": "Segment UUID (get_tree).",
"type": "string"
}
},
"required": [
"action"
]
}🔴jtbd_edit(action, category, folder_id, human_edited, i_want_to, ...)
Edit JTBD graph: extract_interview, move_interview, create_segment, fill_in job, update_job, delete_job.
입력 스키마
{
"type": "object",
"properties": {
"action": {
"enum": [
"extract_interview",
"move_interview",
"create_segment",
"fill_in",
"update_job",
"delete_job"
],
"type": "string"
},
"category": {
"type": "string"
},
"folder_id": {
"type": "string"
},
"human_edited": {
"type": "boolean"
},
"i_want_to": {
"type": "string"
},
"interview_id": {
"type": "string"
},
"job_id": {
"type": "string"
},
"language": {
"description": "Job tree language for extract_interview (en or ru). Defaults to en. Quotes stay in the interview language.",
"type": "string"
},
"name": {
"type": "string"
},
"project_id": {
"type": "string"
},
"segment_id": {
"type": "string"
},
"so_that": {
"type": "string"
},
"to_segment_id": {
"type": "string"
},
"when": {
"type": "string"
}
},
"required": [
"action"
]
}🔴upload_interview
Opens the in-Claude MCP Apps widget "Upload To Zernote" so the user can drop a file into Inbox in this chat (audio/video max 2GB; text .txt/.md or PDF max 50MB). Use when the user wants to upload/add an interview or get started on an empty account — this is the usual ingest path in Claude. Audio/video starts transcription; text/PDF become ready immediately (no speaker diarization). Claude can name interviews after upload.
입력 스키마
{
"type": "object",
"properties": {}
}🟡mcp_prepare_upload(content_type, filename, interview_id, size_bytes, title, ...)
App-only: create an Inbox interview and return a presigned R2 upload URL (or multipart upload_id for large files). Pass interview_id (+ upload_id for multipart) to resume without a new card.
입력 스키마
{
"type": "object",
"properties": {
"content_type": {
"description": "MIME type for the PUT (optional).",
"type": "string"
},
"filename": {
"description": "Original filename.",
"type": "string"
},
"interview_id": {
"description": "Optional: existing empty interview UUID — resume upload without creating a new card.",
"type": "string"
},
"size_bytes": {
"description": "File size in bytes.",
"type": "integer"
},
"title": {
"description": "Optional interview title (defaults to sanitized filename).",
"type": "string"
},
"upload_id": {
"description": "Optional multipart upload id from a previous prepare (resume).",
"type": "string"
}
},
"required": [
"filename",
"size_bytes"
]
}🟡mcp_sign_upload_parts(interview_id, object_key, part_numbers, upload_id)
App-only: return presigned R2 URLs for one or more multipart upload parts (resume-safe).
입력 스키마
{
"type": "object",
"properties": {
"interview_id": {
"description": "Interview UUID from prepare.",
"type": "string"
},
"object_key": {
"description": "R2 object key from prepare.",
"type": "string"
},
"part_numbers": {
"description": "Part numbers to sign (1-based, max 20).",
"items": {
"type": "integer"
},
"type": "array"
},
"upload_id": {
"description": "Multipart upload id from prepare.",
"type": "string"
}
},
"required": [
"interview_id",
"object_key",
"upload_id",
"part_numbers"
]
}🟡mcp_confirm_upload(filename, interview_id, language, multipart_parts, multipart_upload_id, ...)
App-only: confirm a completed R2 upload (single PUT or multipart) for an Inbox interview. Audio/video is enqueued for transcription (STT + diarization). Text (.txt/.md) and PDF are stored and marked ready immediately — no transcription.
입력 스키마
{
"type": "object",
"properties": {
"filename": {
"description": "Original filename.",
"type": "string"
},
"interview_id": {
"description": "Interview UUID from prepare.",
"type": "string"
},
"language": {
"description": "STT language for audio/video (e.g. en, ru, nl). Not used for text/PDF (no transcription).",
"type": "string"
},
"multipart_parts": {
"description": "Completed parts (part_number + etag) when using multipart.",
"items": {
"properties": {
"etag": {
"type": "string"
},
"part_number": {
"type": "integer"
}
},
"type": "object"
},
"type": "array"
},
"multipart_upload_id": {
"description": "Multipart upload id — complete assembly before confirm.",
"type": "string"
},
"object_key": {
"description": "R2 object key from prepare.",
"type": "string"
},
"size_bytes": {
"description": "File size in bytes.",
"type": "integer"
},
"title": {
"description": "Optional title to apply before confirm.",
"type": "string"
}
},
"required": [
"interview_id",
"object_key",
"filename",
"size_bytes",
"language"
]
}커뮤니티
증거