Skip to content

feat(agent-toolkit): fix InvalidFormToken failures in form tools - #480

Open
stasshw wants to merge 1 commit into
masterfrom
feat/form-token-validation
Open

feat(agent-toolkit): fix InvalidFormToken failures in form tools#480
stasshw wants to merge 1 commit into
masterfrom
feat/form-token-validation

Conversation

@stasshw

@stasshw stasshw commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Why

InvalidFormToken is the largest single get_form error bucket in production — 1,285 calls over Aug 24 – Sep 6 — and it is not specific to one client. Per-client error rates on get_form:

Client Calls Errors Error rate InvalidFormToken
ChatGPT 6,719 1,263 18.8% 515
Internal (unlabeled) 14,738 821 5.6% 436
Claude 7,448 514 6.9% 298
Cursor 574 41 7.1% 12
Zeta Labs 339 13 3.8% 11

Inspecting what agents actually sent reveals two distinct root causes.

Cause 1 — agents pass a URL, not the token

Of the 1,285 bad values: 858 bare strings, 353 shortened wkf.ms links, 51 full form URLs, 23 other. The workforms service enforces /^[a-f0-9]{32}$/i before any lookup, so a URL is rejected outright even though the token is sitting inside it.

All four form tools (get_form, update_form, create_form_submission, form_questions_editor) now normalize the input locally through a shared resolveFormToken:

  • a full forms.monday.com/forms/<token>?r=use1 URL is parsed
  • a shortened wkf.ms link is resolved by following the redirect
  • anything that still fails the 32-hex pattern is rejected before the API call, with an error message that restates what a valid token is so the agent can self-correct

That recovers roughly 31% of observed InvalidFormToken calls (404 of 1,285) which previously round-tripped to a hard failure.

Cause 2 — agents had no way to obtain a token

The only field carrying a form token is BoardView.view_specific_data_str, which get_board_info did not request. With no discovery path, agents guessed — passing board ids, view ids, and item ids as formToken.

  • get_board_info now returns view_specific_data as real JSON, parsed from the stringified field. The platform populates it only for form views ({token, disabled, region}, ~70 bytes); every other view type returns "{}", which is omitted entirely, so non-form boards pay nothing.
  • get_board_info accepts filters.views.type, wired to the already-existing (but unused) type argument on the views GraphQL field. An agent can pass {"type": "FormBoardView", "only": true} and reach a token from a board id alone — no view id required, which matters because agents generally do not have one.
  • The form tools point at exactly that call as a [REQUIRED PRECONDITION], following the convention create_column uses for get_column_type_info.

On exposing the token

The token is SecureRandom.hex(16) — a real capability, not an id. But it is a deliberately public one: it is the form's shareable address, and wkf.ms exists to spread it. Returning it adds no new exposure — the resolver is already gated on can?(:read, @board), it is visible in the UI to anyone with board read, and it is documented public API. It also does not bypass features.password.enabled, requireLogin, or isInternal.

Also fixed

update_form's description told agents to call get_form "to resolve the formToken" — circular, since get_form requires the token as input. Reworded to "to confirm the formToken points at the intended form".

Testing

  • 73/73 suites, 1370/1370 tests pass; tsc --noEmit clean; eslint clean.
  • New form-token.test.ts covers bare tokens, full URLs, short-link redirects, redirect failure, and rejection messages.
  • New get-board-info.test.ts cases cover token parsing, never exposing view_specific_data_str, omission for "{}" / unparseable / array / null / missing, and the filters.views.type passthrough (including that it is dropped when the views section is excluded).

Note

Version bumped to 5.68.0 with a CHANGELOG.md entry, per the repo's manual-versioning rule.

🤖 Generated with Claude Code

InvalidFormToken is the largest single get_form error bucket in production
(1,285 calls over two weeks) and it hits every MCP client, not one of them.
Two root causes, both fixed here.

1. Agents pass a form URL instead of the token. All four form tools now
   normalize the input locally via resolveFormToken: a full
   forms.monday.com/forms/<token> URL is parsed, a shortened wkf.ms link is
   resolved by following the redirect, and anything that still fails
   /^[a-f0-9]{32}$/i is rejected before the API call with a message that
   restates what a valid token is. Roughly 31% of observed InvalidFormToken
   calls carried a recoverable URL.

2. Agents had no way to discover a token in the first place, so they guessed
   board ids and view ids. get_board_info now returns view_specific_data as
   real JSON (parsed from view_specific_data_str, which the platform only
   populates for form views, and omitted entirely when empty), and accepts
   filters.views.type so a caller can ask for {"type": "FormBoardView"} and
   reach the token from a board id alone, with no view id needed. The form
   tools point at that call as a [REQUIRED PRECONDITION].

Also fixes a circular instruction in update_form, which told agents to call
get_form "to resolve the formToken" — a call that requires the token as input.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant