A side-by-side look at the Human Survey and LogiSheets MCP servers: transport, auth, tools, quality signals, and the config to install either one in Claude, Cursor, Windsurf or VS Code.
Verdict: choose Human Survey for workplace & productivity work as a local process; choose LogiSheets for workplace & productivity as a local process. You can also run both side by side in one mcpServers block.
HUMANSURVEY_API_KEY.login, get_catalog, list_forms.open_workbook, save_workbook, export_xlsx.Rows where the two servers differ are marked.
| Spec | ||
|---|---|---|
| Category | Workplace & Productivity | Workplace & Productivity |
| Quality signal | 60/100 · Good | 67/100 · Great |
| Install command | Confirmed | Confirmed |
| Transport | Local subprocess (stdio) | Local subprocess (stdio) |
| Auth | Not declared | Not declared |
| Pricing | Not declared | Not declared |
| Required env vars | HUMANSURVEY_API_KEY | None listed |
| Verified owner | Community listing | Community listing |
| GitHub stars | 1 | 41 |
| Directory activity | 2 views 0 installs 0 upvotes | 1 views 0 installs 0 upvotes |
| Tools | 10 | 32 |
| Full listing | View Human Survey | View LogiSheets |
loginSign in with an email address and store an API key on this machine. Called with only an email, it mails a six-digit code and returns nothing usable — the code has to come from the person, out of their inbox. Called with the email and that code, it stores a key at ~/.humansurvey/credentials and reports where it was written. The key itself is never printed. Signing in and signing up are the same act: an address that has never been used gets an account.get_catalogList the platforms HumanSurvey knows: the slug to use as catalog_slug, the display label, and which class of channel it is. These are the named channels a respondent can be shown — TikTok, ChatGPT, LinkedIn, Reddit and so on — plus the descriptive options that have no brand behind them, like "a friend or colleague told me". No API key needed.list_formsList the forms on this account: id, name, respondent URL, whether it is active or paused, whether it has been configured with questions yet, and how many responses it has collected.get_formRead one form: its settings, and the question configuration currently shown to respondents — every question, every candidate, and the order mode. Returns the configuration as JSON, because that is the exact shape a configuration write takes.create_formCreate a form and return its id, its respondent URL, and the iframe and postMessage listener to embed that URL in a host page. The form has no questions yet and shows nothing to respondents until it is configured. allowed_origins restricts which sites may embed it; leaving it empty allows any site, which means any site can spend this account’s response quota.configure_formSet the questions and candidates a form shows. Replaces the whole configuration and stores it as a new immutable version, so responses already collected keep describing the list they were actually shown. Re-sending an identical configuration reuses the existing version rather than making another.
A form is one single-select question, optionally expanding into a second. Give a candidate an `expands` naming another question to reveal it when that candidate is picked — that is how "TikTok" becomes "which account".
Candidate ids must be stable keys that survive a rename. Using a handle as an id splits that creator’s history in two the day they change it.
Set catalog_slug to take a candidate’s label and logo from the platform catalog. Order defaults to "rotate", which randomizes per respondent and is what makes the reported shares unbiased; "fixed" keeps your order and accepts that earlier options get picked more often for being earlier.get_attributionWhat respondents said, aggregated. Reports, per question, how many completed responses named each candidate out of how many answered that question, the unresolved answers in the same list, how often a follow-up question failed to find an answer, and revenue where conversion events have been recorded.
Computed fresh on every call, including any mappings made since — so a rollup of a past window can legitimately change between two reads.list_unresolvedThe answers respondents typed instead of picking from the list, grouped by the exact text and ordered by how often it was typed. Reports the spelling variants seen, when each was first and last typed, and whether a mapping already covers it.
These are verbatim words. Two texts that read alike may be different people, and a text may name somebody who is not on the candidate list at all.remapRecord that a specific typed answer means a specific candidate. It applies to every response already carrying that text as well as every future one, so numbers reported for past windows change.
The original text is never altered and a mapping can be revoked, but a report may already have been read. Only map a text whose meaning is established — a text that merely resembles a candidate’s name is not evidence that it is that candidate.
One live mapping per text per question.revoke_remapStop a mapping from applying. The typed answers it covered return to the unresolved list — in past windows as well as future ones, because rollups join mappings when they are read rather than when they are made. Numbers already reported for those windows will therefore change.
Nothing is deleted: the mapping is marked revoked and kept, so a report produced while it was live can still be explained. Revoking an already-revoked mapping is not an error and does not move the time it stopped applying.
Mapping ids appear beside any text that is already mapped in the free-text listing, and in the confirmation given when a mapping is created.open_workbookStart the workbook you will work in. Call with no arguments for a fresh, empty one; pass `path` to load an existing .xlsx from disk and work on the human's real file.
This replaces whatever workbook the session currently holds, discarding unsaved changes — so call it once at the start, not between steps. You do NOT have to call it at all: an empty workbook appears automatically the moment any other tool touches the session.
Prefer `path` over `xlsx_base64`. `xlsx_base64` exists for hosts with no shared filesystem and costs enormous context for a file of any size.save_workbookWrite the workbook to a real .xlsx file the human can open in Excel. This is how you hand your work back — do it when the task is done.
Defaults to the path it was opened from, or last saved to; pass `path` to write somewhere else. Values, formulas and the block structure are all saved.
IMPORTANT — if the human is going to work on this in Excel, pass `resolve_block_refs: true`. Formulas that read blocks are written as BLOCKREF/BLOCKREFS, which only LogiSheets understands: Excel shows the saved numbers but turns those cells into #NAME? the moment it recalculates. Resolving rewrites them as ordinary A1 references so Excel can recompute the model. Leave it off when the file is coming back here — the named form is readable and survives rows moving.
The result carries a link to the workbook rather than its bytes, so the host can offer the human the file without any of it passing through your context.export_xlsxReturn the workbook as base64-encoded .xlsx bytes.
Last resort. The bytes land in your context and cost roughly 1.4 KB of text per KB of file. Prefer `save_workbook`, which writes a real file and hands the host a link to it — the host can give the human the workbook without any of it passing through you.list_blocksOrient yourself in a workbook: every sheet, every block, and for each block the fields you can reference. One call is enough to start writing formulas.
Per block you get its ref name (BLOCKREF's first argument), its `fields` in column order (the third argument), `key_field` — the column whose values BLOCKREF matches as its second argument — and `derived_fields`, which are computed by a rule and reject writes.
Use `describe_block` when you need a field's actual rule, or the row keys and values. `next_block_start` clears everything already on the sheet, blocks and loose cell content alike, so passing it as `create_block`'s `position` will not land on top of data.
Omit `sheet` to scan the whole workbook; passing it restricts to one sheet.describe_blockReturn a block's full structure for the LLM: identity (name, sheet, position), per-field schema, and row keys in order. Every field reports both what it IS — `type`, `description`, `required`, `unique`, `default_value`, and `references` for a field pointing at another block — and what currently guards it: the `value_formula`, `validation` and `editability` templates. All of it comes from the Rust schema, the engine's authoritative source, so the answer is the same in every host and survives a save/load.
Read a field's `type` and `description` BEFORE writing a value to it: the description is where the unit, the convention, or the thing-not-to-do is written down, and it is the only record of it. A `type` of null means nobody has claimed one yet.
`analyzes` and `analyzed_by` say whether this block is a table or a conclusion drawn from one. A block with `analyzes` set is an ANALYSIS block — a total row, a set of statistics, a pivot — and its rows are its own, not the source's. Never sum an analysis block alongside the block it analyses: that counts the same numbers twice. A field with `aggregates` set is engine-computed from the source; writes to it are dropped, and you change it by changing the declaration.
Pass `include_rows: true` to additionally include current cell values as `rows[].values[fieldName]`. Off by default to save tokens — use it when the agent actually needs to inspect data, not when it only needs the shape.eval_formulaEvaluate an Excel-style formula in a private scratch cell and return the computed value. Nothing is written to user-visible cells. Returns `{type, value}` where type is one of:
- "number" — value is a JS number
- "str" — value is a string
- "bool" — value is a JS boolean
- "error" — value is the Excel error code (e.g. "#REF!", "#NAME?")
- "empty" — value is null (formula returned an empty cell)
Use for:
- Quick checks: "=SUMIFS(OrderStatus, \"amount\", \"*\")" → total
- Sanity-test a candidate template before set_field_rule
- BLOCKREF / BLOCKREFS lookups against any block in the workbook
Leading "=" is optional — it is added automatically if missing.create_blockCreate a structured block (table) on a sheet. fields[0] is the row-key column — the value BLOCKREF matches on, so it has to be unique per record; it is ordinary data you can write. Block ref name (`name`) is used as the first arg to BLOCKREF/BLOCKREFS in formulas.
Field types supported:
- 'string' / 'number' — plain text/numeric cells.
- 'boolean' — cell stores 0/1 or TRUE/FALSE; UI renders ✅/❌ if host has the widget set.
- 'enum' (+ enum_id) — cell stores variant id; UI renders dropdown if host has the widget set. Watson auto-injects a variant-whitelist validation formula on the field so out-of-set writes light up as warnings even without widget rendering. Requires a prior define_enum_set call with matching id.
Rules (value_formula / validation / editability) are set separately via set_field_rule — this call only declares structure + initial rows. Auto-creates the target sheet if missing.convert_to_blockTurn a table that already exists in ordinary cells into a block, in place, without touching its values.
This is how you adopt a workbook someone hands you. `create_block` is for new tables and refuses to write over existing data; this one takes the data as it stands and gives it a name, fields and row keys, so you can address it as (block, row_key, field) and reference it from formulas by name instead of by coordinate.
Give `position` and the counts for the DATA only, leaving out any header row. Field names come from the header row — pass `header_row`, or leave it out and the row directly above the data is used when it looks like titles. `fields` states them outright instead.
The row key is inferred: the first column whose values are all present and all different. Pass `key_field` to choose. Whatever is inferred is reported back, so check it — the key is how formulas and later edits name a row.
Converting is a one-time cost: the block and its schema survive saving and reloading, and afterwards the region behaves exactly like one created as a block.set_block_descriptionWrite what a block is for, in prose, onto the block itself. Saved in the file and returned by describe_block, so it is how the next agent — or the next you, in a later session — learns what the table means.
Say what one row represents, what any non-obvious field holds, and anything that must not be done to it. Do not restate the field names; they are already visible.
Refused when the block reserves `modifyDescription` to its owner. Pass an empty string to clear it.add_block_rowsAdd rows to an existing block. Appends at the end by default; pass after_key or before_key to insert at a position instead. Each row needs a key; values is an object keyed by field name. Fields with a value_formula are auto-materialized by the engine — don't pass them in values. Validation/editability shadows for the new rows are auto-installed by the engine at InsertRowsInBlock time, so no follow-up is needed. Also inserts the matching sheet rows (one block per sheet-row assumption — extends the sheet so downstream rows shift down).delete_block_rowsDelete rows from a block by their key. Missing keys are silently ignored. Also deletes the matching sheet rows (one block per sheet-row assumption). If you try to delete every row, the last one is kept and its cells are cleared instead — the engine doesn't allow rowCnt=0 blocks.move_block_rowReorder a block by moving one row to a new position, addressing both the row and its destination by key. Moves to the end when neither after_key nor before_key is given (same default as add_block_rows). Row order inside a block is presentation only — formulas address rows by key, so reordering never changes a single computed value. Use it to match a reading order someone expects, not to change the model.Paste either block (or both) into your client config, e.g. claude_desktop_config.json or ~/.cursor/mcp.json. We only show a config when the install command is confirmed.
{
"mcpServers": {
"sunsiyuan-human-survey": {
"command": "npx",
"args": [
"-y",
"humansurvey-mcp"
],
"env": {
"HUMANSURVEY_API_KEY": "YOUR_HUMANSURVEY_API_KEY_HERE"
}
}
}
}
{
"mcpServers": {
"logisheets": {
"command": "npx",
"args": [
"-y",
"logisheets-mcp"
]
}
}
}