lark

SKILL.md

Skill: Lark Integration

Prerequisites

  1. MCP server tn-lark: Lark MCP server must be configured in your Claude Code MCP settings
    • Provides mcp__tn-lark__* tools (document read, messaging, contacts, bitable, wiki)
    • See Lark Open Platform docs for server setup
  2. Secrets file: Create skills/lark/lark.secrets.yaml (gitignored):
    app_secret: "<your-lark-app-secret>"
    
    Get the app secret from Lark Developer Console (app ID is in lark-config.yaml)
  3. Dependencies: curl, jq must be available in PATH (used by lark-auth.sh for Direct API calls)
  4. Wiki access (if editing wiki docs): App must be added as wiki space member with edit permission

Purpose

Integrate with Lark/Feishu platform: documents, messenger, contacts, and base (Bitable).

When to Use

  • Reading Lark documents (wiki, docx)
  • Editing Lark documents (text, headings, tables, etc.)
  • Extracting tables from documents
  • Sending messages to chats
  • Looking up user information
  • Querying Lark Base/Bitable data

Configuration

All config files are bundled within this skill:

File Purpose
lark-config.yaml API config, app_id
lark.secrets.yaml app_secret (gitignored)
scripts/lark-auth.sh Token management with caching
lark-token.cache Cached token (gitignored, auto-generated)

Helper Functions

The scripts/lark-auth.sh script provides:

Function Purpose
get_lark_token Get/refresh tenant access token
lark_api METHOD endpoint [data] Make authenticated API call
get_document_blocks doc_id Get single page of blocks
get_all_document_blocks doc_id Get all blocks with pagination
update_block doc_id block_id content Update block text
update_block_styled doc_id block_id elements_json Update with styled text
batch_update_blocks doc_id updates_json Batch update multiple blocks
create_block doc_id parent_id type content Create new block
insert_table_row doc_id table_id row_index Add table row
insert_table_column doc_id table_id col_index Add table column
delete_table_rows doc_id table_id start end Remove table rows
batch_delete_blocks doc_id parent_id start end Delete child blocks by index range
get_wiki_node wiki_token Get wiki node info (for editing)
retry_with_backoff retries delay_ms cmd Retry transient errors

Usage

# Source the auth script (uses relative path internally)
SKILL_DIR="$(dirname "${BASH_SOURCE[0]}")"
source "$SKILL_DIR/scripts/lark-auth.sh"

# Or via repo root
REPO_ROOT="$(git rev-parse --show-toplevel)"
source "$REPO_ROOT/projects/truenorth/skills/lark/scripts/lark-auth.sh"

# Then use functions
TOKEN=$(get_lark_token)
lark_api GET "/docx/v1/documents/$DOC_ID/raw_content"

Document Editing

Overview

Lark documents can be edited via the Direct API (not MCP). All text-type blocks support editing.

Key Points:

  • Use batch update for efficiency (up to 200 blocks per request)
  • Retry with exponential backoff for transient 9499 errors
  • Wiki documents require app to be wiki space member

Edit a Block

source "$SKILL_DIR/scripts/lark-auth.sh"

# Simple text update
update_block "$DOC_ID" "$BLOCK_ID" "New content here"

# With retry for reliability
retry_with_backoff 5 500 "update_block '$DOC_ID' '$BLOCK_ID' 'New content'"

Edit with Styling

# Bold, italic, underline, strikethrough, inline_code
update_block_styled "$DOC_ID" "$BLOCK_ID" '[
  {"content": "Bold text", "bold": true},
  {"content": " normal "},
  {"content": "italic", "italic": true}
]'

Batch Update

batch_update_blocks "$DOC_ID" '[
  {"block_id": "block1", "content": "First update"},
  {"block_id": "block2", "content": "Second update"}
]'

Create Blocks

# Block types: 2=text, 3=h1, 4=h2, ..., 12=bullet, 13=ordered, 14=code, 15=quote, 17=todo
create_block "$DOC_ID" "$PAGE_ID" 2 "New paragraph"
create_block "$DOC_ID" "$PAGE_ID" 3 "New Heading 1"
create_block "$DOC_ID" "$PAGE_ID" 12 "Bullet item"

Table Operations

# Get table block_id from document blocks
insert_table_row "$DOC_ID" "$TABLE_ID" 2      # Insert at row 2
insert_table_column "$DOC_ID" "$TABLE_ID" 1   # Insert at column 1
delete_table_rows "$DOC_ID" "$TABLE_ID" 2 3   # Delete rows 2-3

# Edit table cells: cells are containers with child text blocks
BLOCKS=$(get_document_blocks "$DOC_ID")
CELL_CHILD=$(echo "$BLOCKS" | jq -r '.data.items[] | select(.block_id == "CELL_ID") | .children[0]')
update_block "$DOC_ID" "$CELL_CHILD" "Cell content"

Edit Wiki Documents

Wiki documents use the same API but need the underlying obj_token:

# Get wiki node info
NODE_INFO=$(get_wiki_node "WIKI_NODE_TOKEN")
OBJ_TOKEN=$(echo "$NODE_INFO" | jq -r '.node.obj_token')

# Edit using obj_token
update_block "$OBJ_TOKEN" "$BLOCK_ID" "Updated wiki content"

Note: App must be added as wiki space member with edit permission.

Editable Block Types

Type ID Content Key
Text 2 text
Heading 1-9 3-11 heading1-heading9
Bullet 12 bullet
Ordered 13 ordered
Code 14 code
Quote 15 quote
Todo 17 todo

Container blocks (Callout=19, Table Cell=32, Grid Column=25) hold child text blocks - edit the children.


Commands

/lark read <doc-id>

Read and parse a Lark document with table extraction.

Usage:

/lark read FVcbwfYBFii6ZmkXVUNlj4yFgPg
/lark read https://xxx.larksuite.com/wiki/FVcbwfYBFii6ZmkXVUNlj4yFgPg

What it does:

  1. Fetches all document blocks via Lark Blocks API
  2. Parses block types: Page, Text, Heading, Table, TableCell
  3. Reconstructs table content from nested blocks
  4. Outputs as formatted markdown

/lark message <chat-id> <content>

Send a message to a Lark chat.

Usage:

/lark message oc_xxx "Hello team!"

Implementation: Uses MCP tool mcp__tn-lark__im_v1_message_create


/lark lookup <email>

Look up user information by email.

Usage:

/lark lookup user@example.com

Implementation: Uses MCP tool mcp__tn-lark__contact_v3_user_batchGetId


MCP Tools Available

Tool Purpose
mcp__tn-lark__docx_v1_document_rawContent Get document text
mcp__tn-lark__im_v1_message_create Send message
mcp__tn-lark__im_v1_message_list Get chat history
mcp__tn-lark__im_v1_chat_list List chats
mcp__tn-lark__im_v1_chatMembers_get Get chat members
mcp__tn-lark__contact_v3_user_batchGetId Look up users
mcp__tn-lark__wiki_v2_space_getNode Get wiki node info
mcp__tn-lark__wiki_v1_node_search Search wiki
mcp__tn-lark__bitable_v1_* Lark Base operations

Document Parsing Details

Block Types

Type ID Description Editable
Page 1 Document root Container
Text 2 Plain text paragraph
Heading 1-9 3-11 Section headings
Bullet 12 Unordered list item
Ordered 13 Ordered list item
Code 14 Code block
Quote 15 Quote block
Todo 17 Checkbox item
Callout 19 Callout box Container
Divider 22 Horizontal line N/A
Grid 24 Multi-column layout Container
Grid Column 25 Column in grid Container
Image 27 Image block replace_image
Table 31 Table container Table ops
TableCell 32 Table cell Container

Table Parsing

Tables have nested structure:

Table (31)
└── cells: ["cell_block_id_1", "cell_block_id_2", ...]
    └── TableCell (32)
        └── children: ["text_block_id"]
            └── Text (2)
                └── elements[].text_run.content

Implementation Example

source "$SKILL_DIR/scripts/lark-auth.sh"

# Get all blocks (handles pagination)
BLOCKS=$(get_all_document_blocks "$DOC_ID")

# Parse with jq
echo "$BLOCKS" | jq -r '
  (map({key: .block_id, value: .}) | from_entries) as $blocks |
  .[] |
  if .block_type == 3 then
    "## " + (.heading.elements[0].text_run.content // "")
  elif .block_type == 2 then
    (.text.elements // [] | map(.text_run.content // "") | join(""))
  elif .block_type == 31 then
    "| " + (
      .table.cells |
      map($blocks[.] | .block.children // [] |
        map($blocks[.].text.elements[0].text_run.content // "") |
        join(" ")
      ) |
      join(" | ")
    ) + " |"
  else
    empty
  end
'

Interactive Card Formatting

When sending messages via mcp__tn-lark__im_v1_message_create with msg_type: "interactive":

Column Set (Table-like Layout)

Use column_set for table-like data. Markdown tables don't render properly in cards.

{
  "tag": "column_set",
  "flex_mode": "none",
  "background_style": "grey",
  "columns": [
    {
      "tag": "column",
      "width": "weighted",
      "weight": 3,
      "elements": [{"tag": "markdown", "content": "**Task**"}]
    },
    {
      "tag": "column",
      "width": "weighted",
      "weight": 1,
      "elements": [{"tag": "markdown", "content": "**Owner**"}]
    }
  ]
}

Common Components

Component Tag Use
Text/Markdown markdown Rich text content
Divider hr Horizontal line
Button action with button Clickable links
Columns column_set Table-like layouts

Card Header

{
  "header": {
    "template": "blue",
    "title": {"tag": "plain_text", "content": "Title Here"}
  },
  "elements": [...]
}

Header templates: blue, green, orange, red, purple, grey

Example: Sprint Update Card

{
  "header": {"template": "blue", "title": {"tag": "plain_text", "content": "Sprint Update"}},
  "elements": [
    {"tag": "markdown", "content": "**Summary text here**"},
    {"tag": "hr"},
    {"tag": "column_set", "columns": [...]},
    {"tag": "action", "actions": [
      {"tag": "button", "text": {"tag": "plain_text", "content": "View"}, "type": "primary", "url": "https://..."}
    ]}
  ]
}

Limitations

Reading:

  1. Link previews - Embedded links show as link_preview blocks
  2. Images - Not downloaded; shows placeholder
  3. Complex formatting - Bold, italic, colors simplified to plain text
  4. Comments - Not included in block API response

Editing:

  1. Rate limits - 3 edits/sec per document, 3 API calls/sec per app
  2. Batch size - Max 200 blocks per batch request
  3. Wiki permission - App must be wiki space member with edit permission
  4. Transient errors - Use retry with exponential backoff for 9499 errors

Bitable API Pitfalls (from harvis-3or, harvis-a81)

DateTime Filters

  • Filter value format: ["ExactDate", "<millisecond_timestamp>"] — raw timestamps don't work
  • isGreaterEqual/isLessEqual operators don't work on DateTime fields
  • Use isGreater/isLess with +/-1ms offset instead

Eventual Consistency

  • Reads don't immediately see just-created rows
  • Causes duplicate creation if you read-then-write without guards
  • Workaround: Maintain an in-memory cache and check before creating
  • Use asyncio.Lock (or equivalent) to prevent concurrent write races

Field Type Surprises

  • Text fields return as rich-text arrays, not plain strings
  • Date fields accept millisecond timestamps for writes
  • Always validate API response shapes before building — they differ from docs

Record Search

  • POST /bitable/v1/apps/:app/tables/:table/records/search for queries
  • Filter conditions: {"field_name": "x", "operator": "is", "value": ["y"]}
  • Batch create: POST /bitable/v1/apps/:app/tables/:table/records/batch_create

Related

  • /pjm skill - Uses this for reading sprint docs
  • Team members in shared/team/members.yaml have Lark IDs
Weekly Installs
1
Repository
popodidi/harvis
First Seen
5 days ago
Installed on
mcpjam1
claude-code1
replit1
junie1
windsurf1
zencoder1