使用说明
kanbox-mcp
MCP server for the Kanbox LinkedIn CRM API. Exposes two dispatcher tools — kanbox_read (4 actions) and kanbox_write (6 actions) — for use with Claude Code and other MCP clients.
Use cases
- CRM triage — Search unread inbox messages, read conversations, then draft and send replies — all from your editor.
- Pipeline management — Query contacts by pipeline/step, update labels or move contacts between steps based on conversation context.
- Outreach automation — Build a lead list, send personalized connection requests with icebreakers, and follow up with messages once connected.
- Lead enrichment — Add leads by LinkedIn URL for full background enrichment, then poll processing status until complete.
- Contact lookup — Resolve a LinkedIn profile slug to the internal
linkedin_idneeded for messaging, without leaving your workflow. - Refresh a contact — Re-scrape a single profile on demand and get the fresh, normalized record back.
Design
This server uses a two-tool dispatcher pattern instead of exposing 9 separate MCP tools. Inspired by Jesse Vincent's article on MCP API design, the goals are:
- Minimal context footprint — LLM clients load every tool definition into their context window. Two tools with action routing costs far less context than nine individual tool definitions, leaving more room for the actual conversation.
- Self-documenting — Each tool's description includes the full action catalog with parameter names and gotchas. The LLM gets everything it needs from the tool definition itself, without requiring external documentation in the system prompt.
- Read/write permission split — Two tools instead of one lets MCP clients auto-approve
kanbox_readwhile requiring explicit confirmation forkanbox_write. - Flat responses — The Kanbox API nests contact data under
lead.*andlnuser.*keys. This server flattens responses so fields likelinkedin_id,firstname,emailappear at the top level, reducing the tokens the LLM spends parsing nested structures.
Key response fields
search_members returns the richest data — these are your inbox/connection contacts:
id,linkedin_id,linkedin_public_id,firstname,lastname,phone,company,job,location,labels,pipeline,step,icebreaker,custom,conversations(withid,last_message,unread_count)
With include_history: true, members also carry years_of_experience, the current-position dates (year_position, month_position, startyear_position, startmonth_position), and past_positions — an array of prior roles, each with title, company, company_linkedin_url, location, description, start_year/start_month, end_year/end_month, and is_current. These are omitted by default to keep responses compact; request them only when reconstructing a contact's job changes.
search_leads returns scraped lead data (no linkedin_id — resolve via search_members):
linkedin_public_id,firstname,lastname,phone,company,job,location
list_lists returns list metadata:
name,count,is_processing
Setup
In Kanbox Settings, turn on "Enable API Key" in the "API" section, then set the token in your environment:
export KANBOX_API_TOKEN=your_api_key
Claude Code configuration
Add to ~/.claude/settings.json:
{
"mcpServers": {
"kanbox": {
"command": "npx",
"args": ["-y", "github:flacoste/kanbox-mcp"],
"env": {
"KANBOX_API_TOKEN": "${KANBOX_API_TOKEN}"
}
}
}
}
No local clone required — npx fetches the repo, installs dependencies, and builds automatically.
Local install
If you prefer a local copy:
git clone https://github.com/flacoste/kanbox-mcp.git
cd kanbox-mcp
npm install # also builds via prepare script
Then point the config at the local build:
{
"command": "node",
"args": ["/path/to/kanbox-mcp/dist/index.js"],
"env": {
"KANBOX_API_TOKEN": "${KANBOX_API_TOKEN}"
}
}
Tools
kanbox_read
Read-only queries against Kanbox data.
search_members
Search inbox, connections, or unread messages.
| Param | Type | Required | Description |
|---|---|---|---|
q |
string | no | Fuzzy name search (a single distinctive term works best) |
type |
inbox | unread_inbox | connections |
no | Filter by member type |
pipeline_name |
string | no | Filter by pipeline name |
step_title |
string | no | Filter by pipeline step title |
linkedin_public_ids |
string[] | no | Public profile slugs for exact lookup |
updated_since |
string | no | ISO 8601 timestamp filter |
limit |
number | no | Max results (1-100) |
offset |
number | no | Results to skip |
include_history |
boolean | no | Include position history (past_positions, years_of_experience, position dates). Default false — off keeps output compact |
search_leads
Search scraped leads by list name or query.
| Param | Type | Required | Description |
|---|---|---|---|
name |
string | no | List name filter |
q |
string | no | Search query |
limit |
number | no | Max results (1-100) |
offset |
number | no | Results to skip |
get_messages
Get conversation messages.
| Param | Type | Required | Description |
|---|---|---|---|
conversation_id |
number | yes | Conversation ID from search_members conversations[].id |
cursor |
string | no | Pagination cursor from previous response |
list_lists
List available Kanbox lead lists.
| Param | Type | Required | Description |
|---|---|---|---|
limit |
number | no | Max results (1-100) |
offset |
number | no | Results to skip |
kanbox_write
Write operations. Most writes are async (HTTP 202 Accepted); refresh_lead returns synchronously (HTTP 200).
update_member
Update member fields.
| Param | Type | Required | Description |
|---|---|---|---|
id |
number | yes | Member ID from search_members |
email |
string | no | Email address |
phone |
string | no | Phone number |
labels |
string[] | no | Full replacement — pass ALL desired labels |
pipeline |
string | no | Pipeline name |
step |
string | no | Pipeline step |
custom |
string | no | Custom note |
icebreaker |
string | no | Icebreaker note |
send_message
Send a LinkedIn message.
| Param | Type | Required | Description |
|---|---|---|---|
recipient_linkedin_id |
string | yes | Internal LinkedIn ID (ACoAAA... format) |
message |
string | yes | Message text (max 8000 chars) |
send_connection
Send a connection request.
| Param | Type | Required | Description |
|---|---|---|---|
recipient_linkedin_id |
string | yes | Internal LinkedIn ID (ACoAAA... format) |
message |
string | no | Request message (max 300 chars) |
add_lead
Add a structured lead to a list (partial enrichment only).
| Param | Type | Required | Description |
|---|---|---|---|
list |
string | yes | Kanbox list name |
linkedin_public_id |
string | yes | Public profile slug |
firstname |
string | yes | First name |
lastname |
string | yes | Last name |
email |
string | no | Email address |
phone |
string | no | Phone number |
company |
string | no | Company name |
location |
string | no | Location |
job |
string | no | Job title |
icebreaker |
string | no | Icebreaker note |
labels |
string[] | no | Labels |
pipeline |
string | no | Pipeline name |
step |
string | no | Pipeline step |
add_lead_url
Add a lead by LinkedIn URL for full enrichment (takes minutes).
| Param | Type | Required | Description |
|---|---|---|---|
linkedin_profile_url |
string | yes | Full LinkedIn profile URL |
list |
string | yes | Kanbox list name |
refresh_lead
Re-scrape a single contact and return the refreshed, normalized member record. Synchronous (HTTP 200), unlike the async writes above. Accepts a public slug or a full profile URL — not the Kanbox lead_id. The record always includes the full position history (past_positions, years_of_experience, position dates) — unlike search_members, which gates that behind include_history to keep list responses compact; a single refreshed record carries no such cost. This also resolves a scraped lead into a messageable member: refreshing a pure lead creates its member record, returns the internal linkedin_id, and makes it appear in search_members.
| Param | Type | Required | Description |
|---|---|---|---|
identifier |
string | yes | LinkedIn public slug (e.g. janedoe) or full profile URL |
Key conventions
linkedin_idvs public slug —search_membersreturns alinkedin_idfield (internal ID starting withACoAAA...). Use this ID forsend_messageandsend_connection, not the public profile slug.- Labels are full replacement —
update_memberlabels replace the entire label set. Read existing labels first, then pass the complete desired list. - Async writes — Most write operations return HTTP 202 and may take a few seconds to appear on subsequent reads.
refresh_leadis the exception: it returns HTTP 200 synchronously with the refreshed record. - Leads vs members —
search_leadsdoes not returnlinkedin_id, and a pure lead is not visible tosearch_members. Resolve it withrefresh_lead(slug or URL), which creates the member record and returns the internallinkedin_idneeded for messaging.
Development
npm run build # tsc -> dist/
npm run dev # tsx src/index.ts
npm test # vitest run
npm run test:watch # vitest (watch mode)