# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Feishu (飞书) MCP server providing tools for Bitable (multidimensional tables) and Sheets (spreadsheet). Built with FastMCP framework and lark-oapi SDK.
**Current Status**: Bitable tools fully implemented (10 tools). Sheets tools planned (17 tools, not yet implemented).
## Critical Architecture Details
### Standard Package Structure
**Important**: This project uses `src/yuppie_mcp_feishu/` as a proper Python package with absolute imports.
Import examples:
```python
# Correct - absolute imports from package
from yuppie_mcp_feishu.tools import register_bitable_app_tools
from yuppie_mcp_feishu.client import get_client
from yuppie_mcp_feishu.config import get_config
# Wrong - no relative imports within the package
# from .tools import ... # NO
# from tools import ... # NO - requires full package path
```
### Three-Layer Tool Architecture
All tools follow this pattern for testability and reusability:
1. **Implementation Layer** (`_xxx_impl()`)
- Pure API call functions
- Return raw response objects from lark-oapi
- Can be tested independently
2. **Export Layer** (`xxx()`)
- Wrappers that call impl functions
- Serialize responses with `lark.JSON.marshal(response.data, indent=4)`
- Check `response.success()` and return error details if failed
- These are imported by `test_integration.py`
3. **MCP Tool Layer** (`xxx_tool()`)
- Registered with `@mcp.tool()` decorator
- Decorated with `@handle_feishu_error`
- Return raw response objects (decorator handles serialization)
Example from `yuppie_mcp_feishu/tools/bitable_record.py`:
```python
# Layer 1: Implementation
def _create_bitable_record_impl(app_token: str, table_id: str, fields: dict):
client = get_client()
request = CreateAppTableRecordRequest.builder()...
return client.bitable.v1.app_table_record.create(request)
# Layer 2: Export (for testing)
def create_bitable_record(app_token: str, table_id: str, fields: dict) -> str:
response = _create_bitable_record_impl(app_token, table_id, fields)
if not response.success():
error_data = {"success": False, "code": response.code, "msg": response.msg}
return lark.JSON.marshal(error_data, indent=4)
return lark.JSON.marshal(response.data, indent=4)
# Layer 3: MCP tool
@mcp.tool()
@handle_feishu_error
def create_bitable_record_tool(app_token: str, table_id: str, fields: dict):
return _create_bitable_record_impl(app_token, table_id, fields)
```
### Lark-OAPI Builder Pattern Gotchas
The lark-oapi SDK uses builder pattern with **inconsistent method names** across request types. Always check the actual builder:
```python
# BatchCreate/BatchUpdate: .records()
BatchCreateAppTableRecordRequestBody.builder().records(records).build()
# BatchGet: .record_ids() (plural!)
BatchGetAppTableRecordRequestBody.builder().record_ids(record_ids).build()
# BatchDelete: .records() (not record_ids!)
BatchDeleteAppTableRecordRequestBody.builder().records(record_ids).build()
```
### API Response Structure Variations
Different Feishu APIs return different response structures:
```python
# search_bitable_records: items at root
data.get("items", []) # NOT data.get("data", {}).get("items")
# batch_create_bitable_records: records at root
data.get("records", []) # NOT data.get("data", {}).get("records")
# create/update/single operations: varies
data.get("record", {}) or data.get("app", {})
```
### Important Import for JSON Serialization
When serializing lark-oapi responses, import explicitly to avoid conflicts:
```python
from lark_oapi import JSON as LarkJSON
response_data = json.loads(LarkJSON.marshal(response.data))
```
## Development Commands
### Environment Setup
```bash
# Install dependencies
uv sync --all-extras # includes pytest for testing
# Configure credentials
cp .env.example .env
# Edit .env with FEISHU_APP_ID and FEISHU_APP_SECRET
```
### Running
```bash
# Run MCP server (installed package)
yuppie-mcp-feishu
# Or via uvx (uses installed package from PyPI)
uvx yuppie-mcp-feishu
# Or directly with Python module
python -m yuppie_mcp_feishu.main
# Check server initialization
python test_mcp.py
# Check configuration
python check_config.py
```
### Testing (Pytest-based)
The project uses pytest for testing with a modular structure:
```bash
# Run all tests
pytest tests/ -v
# Run specific module
pytest tests/yuppie_mcp_feishu/tools/test_bitable_app.py -v
# Run specific test function
pytest tests/yuppie_mcp_feishu/tools/test_bitable_record.py::test_create_bitable_record -v
# Run with coverage
pytest tests/ --cov=src/yuppie_mcp_feishu --cov-report=html
```
**Required Environment Variable for Testing:**
```bash
FEISHU_DEFAULT_APP_TOKEN=your_app_token_here # Required - specify a test Bitable app
```
Tests use pytest fixtures with automatic dependency injection:
- `default_app_token`: Reads from `FEISHU_DEFAULT_APP_TOKEN` (session-scoped, shared across tests)
- `test_table_id`: Creates/retrieves a test table (session-scoped)
- `test_record_id`: Creates a test record (function-scoped, per-test)
- `test_record_ids`: Batch creates test records (function-scoped, per-test)
**Important**: Application-level tests (create/copy apps) have been removed from the automated test suite. All tests run in the specified default Bitable app to avoid creating duplicate applications.
## Project Structure
```
src/
└── yuppie_mcp_feishu/ # Main package (src layout)
├── __init__.py # Package entry point, exports main()
├── main.py # MCP server creation and configuration
├── config.py # Pydantic config, loads from env
├── client.py # lark-oapi client singleton with token management
├── exceptions.py # @handle_feishu_error decorator
└── tools/
├── __init__.py # Imports all tool registration functions
├── bitable_app.py # 3 tools: create/copy apps, create tables
└── bitable_record.py # 7 tools: record CRUD operations
tests/ # Pytest test suite (mirrors src structure)
├── conftest.py # pytest fixtures configuration
└── yuppie_mcp_feishu/
├── tools/
│ ├── test_bitable_app.py # Table operation tests
│ └── test_bitable_record.py # Record CRUD tests
└── integration/
└── test_bitable_workflow.py # Workflow integration tests
test_integration.py # Legacy integration test (deprecated)
test_mcp.py # Server init tests
check_config.py # Config validation helper
```
## Client and Token Management
The Feishu client (`src/yuppie_mcp_feishu/client.py`) manages tenant_access_token automatically:
- Token lifespan: 2 hours
- SDK handles refresh automatically
- Client is a singleton via `get_client()`
- Uses `enable_set_token(False)` to let SDK manage tokens
Never manually set or refresh tokens - just call `get_client()`.
## Error Handling Pattern
All MCP tools MUST use the `@handle_feishu_error` decorator from `src/yuppie_mcp_feishu/exceptions.py`:
```python
@mcp.tool()
@handle_feishu_error
def your_tool(param: str):
# Decorator catches non-success responses
# Returns JSON error with code, msg, error details
...
```
The decorator:
1. Checks `response.success()` after tool executes
2. If failed, raises `FeishuError` with code/msg/error
3. If success, returns `lark.JSON.marshal(response.data, indent=4)`
## Adding New Tools
Follow the three-layer pattern:
```python
# src/yuppie_mcp_feishu/tools/your_feature.py
# Layer 1: Implementation
def _your_operation_impl(param1: str, param2: str):
from yuppie_mcp_feishu.client import get_client
client = get_client()
request = YourRequest.builder().param1(param1)...
return client.api.v1.resource.your_method(request)
# Layer 2: Export (optional, for testing)
def your_operation(param1: str, param2: str) -> str:
response = _your_operation_impl(param1, param2)
if not response.success():
error_data = {"success": False, "code": response.code, "msg": response.msg}
return lark.JSON.marshal(error_data, indent=4)
return lark.JSON.marshal(response.data, indent=4)
# Layer 3: MCP tool registration
def register_your_feature_tools(mcp: FastMCP):
from yuppie_mcp_feishu.exceptions import handle_feishu_error
@mcp.tool()
@handle_feishu_error
def your_operation_tool(param1: str, param2: str):
"""Tool description here"""
return _your_operation_impl(param1, param2)
```
Then in `src/yuppie_mcp_feishu/main.py`:
```python
from yuppie_mcp_feishu.tools import register_your_feature_tools
def create_mcp_server() -> FastMCP:
mcp = FastMCP("yuppie-mcp-feishu", json_response=True)
register_your_feature_tools(mcp)
return mcp
```
And in `src/yuppie_mcp_feishu/tools/__init__.py`:
```python
from yuppie_mcp_feishu.tools.your_feature import register_your_feature_tools
```
## MCP Client Configuration
### Claude Desktop
`~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"feishu": {
"command": "uvx",
"args": ["yuppie-mcp-feishu"],
"env": {
"FEISHU_APP_ID": "your_app_id",
"FEISHU_APP_SECRET": "your_app_secret"
}
}
}
}
```
### Cherry Studio
Use STDIO type with:
- Command: `uvx`
- Args: `yuppie-mcp-feishu`
- Env: `FEISHU_APP_ID`, `FEISHU_APP_SECRET`
### Remote Deployment (阿里云, etc.)
For cloud deployments, `uvx yuppie-mcp-feishu` works directly since the package is now properly structured:
```json
{
"mcpServers": {
"yuppie-mcp-feishu": {
"command": "uvx",
"args": ["yuppie-mcp-feishu"],
"env": {
"FEISHU_APP_ID": "cli_a83d2667c93dd00d",
"FEISHU_APP_SECRET": "gznbWwHCAHx9g1ExGDNK0gv0Kdu46o4j"
}
}
}
}
```
## Dependencies
- **UV**: Package manager (not pip/poetry)
- **mcp[cli]**: FastMCP framework
- **lark-oapi**: Feishu Python SDK
- **pydantic**: Config validation
- **python-dotenv**: Env loading
- **pytest**: Testing framework (dev dependency)
- **Python**: >= 3.10
## Planned Features (需求文档.md)
Sheets API has 17 endpoints planned but not yet implemented:
- Spreadsheet operations (create, get)
- Sheet operations (add, copy, delete, update, query)
- Row/column operations (add, insert, update, move, delete)
- Data operations (read, write, append, prepend, images)