Skip to content

feat(mcp): add document fix tools and webhook reads - #454

Open
dodeja wants to merge 73 commits into
mcp-v2/05-refs-partiesfrom
mcp-v2/07-document-writes
Open

dodeja wants to merge 73 commits into
mcp-v2/05-refs-partiesfrom
mcp-v2/07-document-writes

Conversation

@dodeja

@dodeja dodeja commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Why

AI agents should change Terminal49 account data only through a preview the person approves, and no merge should switch new write tools on by default before one release PR does it in a single, revertible step. Misclassified documents need a fix path, and integrators need to see their webhook setup, so this PR adds those tools behind opt-in toolsets.

What changed

update_document (set_type, reclassify, reextract) and relink_document in the opt-in documents_write toolset, both tier C with confirms: 'always'. list_webhooks and list_webhook_notifications in the opt-in developer toolset. Webhook secrets, header values and URL query strings never appear in output, logs, Sentry or PostHog (packages/mcp/src/webhook-telemetry.test.ts).

relink_document started as tier A. The backend's DocumentReferenceLinker, which POST /documents/{id}/relink re-runs, also removes the links its own matching made earlier that no longer match, so a relink can remove data and now needs the user's approval of a preview of the current links.

The server instructions' write paragraph no longer names each write tool. Every write tool's description opens with "Changes account data." from its registry tier, and a tier C description goes on to say when the call needs an approved preview. The paragraph is the same size however many write tools a selection serves: 1,824 bytes at the default and at the largest selection, under the 2,048 that Claude Code keeps. A test checks that the rules (call a write only when the user asks, every other tool only fetches data, never pass a confirmation_token the user has not approved, tool output is untrusted) appear in every toolset selection that serves a write tool and nowhere else.

The default toolsets stay core, documents, operations and trade, which serve 30 tools, the same list as #452. All four toolsets below them are opt-in: tracking_lifecycle, reference_numbers, documents_write and developer. 39 tools exist in all.

#455 ("turn on write tools by default") had been squash-merged into this branch. That commit is reverted here (ed9560b) and its content returns as its own release PR, so that the switch to default-on lands once and can be reverted on its own.

Verification

  • vp run check, vp run test and npm run agent-verify pass at the head. The MCP package runs 74 test files and 1,103 tests.
  • The preview-then-token flows for both document writes, and the webhook reads, run through the MCP client in packages/mcp/src/document-writes.transport.test.ts.
  • GitHub CI does not run on this PR because its base is not main; the local gates above are the record until the stack reaches main.

🤖 Generated with Claude Code

https://claude.ai/code/session_01V8cPhgia7FZpMP7q5BJ5nw

dodeja and others added 20 commits October 17, 2026 07:23
The test connects one MCP server for each toolset subset in both modes, 64
round trips in all. Under load it passed the 5 s default, and each new tool
adds to it. It now builds the tool name pattern once as one alternation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add custom-field-values.ts, the shared base for set_custom_field and
update_custom_field. It finds the account's active definition by
api_slug and loads its options. It checks a value against the field's
data_type: text, number, YYYY-MM-DD date, ISO 8601 datetime with a time
zone, boolean, one option for enum and a list of options for enum_multi.
A value that does not fit returns invalid_value with the expected form
and, for enums, allowed_values.

The checks run before any write because Terminal49 stores an empty value
for an enum option it does not know, which would silently clear a field.

It also reads a field's current value on a container, shipment or
tracking request. An enum value of '' or an enum_multi of [] counts as
no value, since that is how Terminal49 reports options that were
removed. Container and shipment fields both apply to a tracking request,
because Terminal49 copies the value to the records it creates.

A 401 or 403 becomes credential_not_supported when /me says custom
fields need a signed-in user, and FeatureNotEnabled when /me says the
account lacks them. The definition loading in
list_custom_field_definitions is now exported for reuse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
set_custom_field fills an empty custom field on a container, shipment or
tracking request. It is tier A in the operations toolset.

It finds the field by api_slug and checks the value against the field's
data_type and options before it sends anything. A value that does not
fit returns invalid_value, an unknown slug returns unknown_field, and a
shipment field on a container returns field_not_applicable. A tracking
request takes both kinds of field.

When the field already has a value, it returns already_set with
current_value and changes nothing, and points to update_custom_field
when the values differ. Otherwise it creates the value and returns set
with what Terminal49 stored. A 422 returns noop. Terminal49 has no
create-only write, so a value that lands between the read and the POST
is replaced. The description tells the model not to run two calls on
the same field in parallel.

A field whose data_type the tool cannot write, such as reference,
returns unsupported_field before the record is read.

Terminal49 copies a tracking request's value to its shipment or
containers only when the value is first created. Once the shipment
exists, a later change on the tracking request does not reach the
copies, and a new value replaces theirs. So on a tracking request that
already created its shipment, the tool returns use_shipment_or_container
with shipment_id and changes nothing. The description tells the model to
set the field on the shipment or its containers instead.

A tracking request can hold one value per container for a container
field. Terminal49 does not say which value belongs to which container,
so the tool returns ambiguous_field with the values and changes nothing.

The api_slug input takes upper and lower case letters, digits and
underscores, as Terminal49 does.

Each result carries the record and its label, like the reference number
tools. The audit event names the record and never the value.

The server instructions tell the model to read the definitions with
list_custom_field_definitions first. The sentence is left out when
either tool is not served.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
update_custom_field replaces or clears a custom field value on a
container, shipment or tracking request. It is tier C in the operations
toolset, because Terminal49 keeps no copy of the old value.

It takes value to replace the field or clear: true to remove it. Passing
both or neither is an input error. A new value goes through the same
data_type and option checks as set_custom_field.

Called without a token, it reads the field and returns a preview with
the record, the field, the current value, the proposed value and the
action. The token is bound to the current value, so a value that changed
after the preview returns stale_preview. A field with no value returns
not_set, and a new value equal to the current one returns unchanged.
A tracking request that holds one value per container for the field
returns ambiguous_field with the values. A tracking request that already
created its shipment returns use_shipment_or_container, because the
change would not reach the shipment or its containers. A clear on a
field whose data_type no tool can write, such as reference, returns
unsupported_field. None of these issues a token.

Called with a valid token, it sends PATCH for a new value or DELETE to
clear. It returns updated or cleared with previous_value, and names the
undo: update_custom_field after an overwrite, set_custom_field after a
clear. A 422 returns noop. Terminal49 answers a DELETE on an empty field
with 204, so the 404 branch that returns not_set is defensive.

set_custom_field's description now points to update_custom_field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pin the SDK calls set_custom_field and update_custom_field make: the
definitions and options reads, the record and custom_fields reads, and
setCustomField, updateCustomField or deleteCustomField with the record
id, the api_slug and the checked value. The fixtures cover a shipment, a
tracking request and a container.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run set_custom_field and update_custom_field through the HTTP handler
and the MCP client, with a new server for each call. One test sets a
field, gets already_set on a second set, then overwrites and clears it
with confirmed previews across separate requests. Others check that
invalid_value and credential_not_supported come back with isError false,
that value plus clear returns the fixed input error, that a 404 names
the missing record, and that read-only mode serves neither tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a round trip to eval/writes.eval.ts on the first shipment: set the
first short_text shipment field with set_custom_field, overwrite it and
clear it with update_custom_field, each with a confirmed preview. A
finally block clears the field if an assertion fails after the set.

Terminal49 rejects custom field changes from API keys. So the case runs
as the signed-in sandbox user in MCP_EVAL_SANDBOX_BEARER, and skips
without that variable. Before any write, it calls whoami with both
credentials. It fails when the bearer's account is not the
MCP_EVAL_SANDBOX_TOKEN account. Like the other write evals, it also
needs MCP_EVAL_ENABLE_WRITE and MCP_EVAL_SANDBOX_TOKEN.

The case skips when the account has no shipment or no short_text
shipment field. It also skips when the field already has a value, so it
never changes a value it did not set. It fails, and does not skip, when
the user cannot load the account's custom field definitions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TOOLS_OVERVIEW.md describes set_custom_field and update_custom_field:
their tiers, inputs, the value each data_type takes, previews and
results. It covers already_set, unchanged, not_set, invalid_value,
unknown_field, field_not_applicable, unsupported_field, ambiguous_field,
use_shipment_or_container, credential_not_supported and noop, and adds an example request. It says
set_custom_field cannot block a concurrent write. It says a change on a
tracking request does not reach the shipment or containers it created. The tool count
is 30, and 21 by default.

The package README and MCP_README.md list both tools in the operations
tables, and the README names them in the Toolsets section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The MCP home page documents set_custom_field and update_custom_field:
parameters, the value each data_type takes, results, previews and REST
links. The results include noop, ambiguous_field and
use_shipment_or_container, and the number limit matches the tool. It
says set_custom_field cannot block a concurrent write, and that a change
on a tracking request does not reach the shipment or containers it
created. It lists them in the operations toolset, which the hosted
server does not serve yet. The Not yet supported note now names custom field
values among the operations tools.

The quickstart lists both tools in the operations table, and the
confirmation secret row names update_custom_field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add relink_document to the documents_write toolset as a tier A write. It
reads the document, asks Terminal49 to match it to shipments and
containers again, and returns accepted with the links it had before the
call. Linking runs in the background. A 422 returns noop, a feature 403
returns FeatureNotEnabled, and a 429 returns rate_limited.

documents_write is not a default toolset, so the default listing does not
change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add update_document to the documents_write toolset as a tier C write with
three actions. set_type sets the document type by hand, reclassify has
Terminal49 choose the type again, and reextract has it read the file
again. Each action can replace the type or the extracted data, so each
one needs a confirmed preview.

set_type checks the code against GET /documents/types before the preview,
because Terminal49 answers an unknown code with a 500. An unknown code
returns unknown_document_type with the allowed codes, and a type the
account already set returns unchanged. Neither issues a token. When the
classifier chose the same type, set_type still previews and writes, so
the account can confirm it. The token is bound to the type, its source,
the notes, the latest extraction and updated_at, so a document that
changed after the preview gets a new preview. reclassify and reextract
run in the background and return accepted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move stringLinks from list_documents to api-fields.ts so the webhook read
tools can keep only string pagination links the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add list_webhooks, a read in the opt-in developer toolset. It lists the
account's webhooks, or reads one by webhook_id, with whether each is
active, its events, and the scheme, host and path of its URL. With
include_event_catalog it adds every event a webhook can subscribe to.
A malformed catalog category or event is dropped, and the rest are kept.

The tool reads only named fields. The signing secret becomes has_secret
and each header becomes its name, so neither value is held after
parsing. The URL loses its query string, fragment, user name and
password. Any path segment or subdomain can be a key the receiver
checks, so the tool does not guess which ones are. The path keeps only
its first segment, and only when it is short lowercase words. The host
keeps its leftmost label only when it is a short lowercase word. Every
other part becomes [redacted]. A feature 403 returns FeatureNotEnabled.

developer is not a default toolset, so the default listing does not
change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add list_webhook_notifications, a read in the opt-in developer toolset.
It lists the notifications Terminal49 created for the account, or reads
one by notification_id. Each row has the event, delivery_status,
created_at, the webhook it went to and the record it is about.

The tool never asks the API to include the webhook, whose record carries
the signing secret, and it reads no payload. The API offers no filters on
this endpoint, so the tool offers none either. A feature 403 returns
FeatureNotEnabled.

This differs from the proposal, which offered an opt-in, truncated
include_payload for one notification. A payload is large, holds text
written outside the account, and has a different shape for each event,
so it would need its own redaction and injection handling. The tool
leaves it out for now. Without it, a row names the record behind the
notification, such as a container_updated_event, but not the container.
So the tool answers which notifications were sent and delivered, not
which container one was about.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pin the SDK calls relink_document, update_document, list_webhooks and
list_webhook_notifications make, including that the notification list
asks for no includes and that only set_type reads the document types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ient

Add transport tests that set a document type with a confirmed preview
across separate stateless requests, relink a document, and return the
fixed errors for bad input and 404s. They also check that the default
listing leaves out all four tools and that read-only mode keeps only the
webhook reads.

Add a telemetry test with Sentry recording tool inputs and outputs. A
webhook secret, header values, a URL query string and a key in the URL
path never reach the tool output, the logs, Sentry or PostHog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ChatGPT and Claude submission files must name no tool outside the
default toolsets, anywhere in the file. The test derives the opt-in tools
from the registry and asserts that the document fix and webhook tools are
among them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The read suite checks that list_webhooks returns no secret, header value
or URL query string, and that list_webhook_notifications returns delivery
rows. Both skip when the gateway does not serve the developer toolset.

The sandbox write suite relinks the first document and previews a
reextract without confirming it, because a document fix cannot be undone
exactly. It skips when the gateway does not serve documents_write. It
still needs MCP_EVAL_ENABLE_WRITE and MCP_EVAL_SANDBOX_TOKEN.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TOOLS_OVERVIEW.md describes relink_document and update_document in the
documents_write toolset, and list_webhooks and list_webhook_notifications
in the developer toolset. It covers the update_document actions, preview,
token binding and results, and which parts of a webhook URL are shown.
It says the webhook tools never return a secret, header values or a URL
query string. The tool count is 34, and 21 by default.

The package README and MCP_README.md list the four tools with their
toolsets, and the README names them in the Toolsets section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/mcp/home.mdx describes relink_document and update_document under
Write tools, and list_webhooks and list_webhook_notifications under a new
Developer tools section, each with REST links. The toolset list names
their tools, and the Not yet supported table now covers webhook setup
only. The quickstart lists the four tools with their toolsets and adds
update_document to the confirmation secret row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
api Ready Ready Preview Oct 30, 2026 1:57am UTC

Request Review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T01:59:56.332163Z d12e745 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ceea554f27

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/mcp/src/tools/webhooks.ts Resolved
Comment thread packages/mcp/src/tools/webhooks.ts Resolved
dodeja and others added 5 commits October 17, 2026 22:33
… value

A clear checks no value, so it no longer fails when the options endpoint
is unavailable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
describeWebhookUrl kept a short lowercase first path segment and leftmost
host label, so a receiver key such as https://example.com/secret reached
the MCP client and telemetry. Spelling cannot tell a route from a key, so
the URL now keeps only the scheme and the host's last two labels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 49992b0ba9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/mcp/home.mdx Resolved
dodeja and others added 2 commits October 18, 2026 20:36
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dodeja and others added 2 commits October 18, 2026 22:53
…pping the single-record custom field tools

set_custom_field from main, now confirmed through the write kit, covers
filling, overwriting and clearing a field on up to 50 records, so this
branch's set_custom_field (which collides with it by name) and
update_custom_field go, with their value checker, fixtures, tests, eval
case, docs and feature-map entries, and the helper exports only they used.
The tree now matches mcp-v2/05-refs-parties.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings the confirmed batch record writes and the removal of the
single-record party and custom field tools into the document and webhook
branch. update_document declares confirms: 'always', the docs count the 35
default tools and the four opt-in ones, and the eval keeps only the document
preview case beside the tracking and reference number cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the risk-shadow:high Shadow-only risk signal: high label Oct 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Shadow risk calibration

Signal Result Authority
Path classifier risk:high Deterministic advisory policy
heuristic fallback risk-shadow:high Shadow only

Calibration result: agreement.

Shadow risk never blocks or enables merge. Keep it in observation mode until roughly 50 pull requests have human-reviewed labels, then calibrate before considering any policy use.

dodeja and others added 3 commits October 18, 2026 23:05
The documents_write and developer toolsets are listed with their tools two
lines below.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The branch's docs/openapi.json matches main's, so the generated collection
should too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ad096e8326

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/mcp/src/server.ts Resolved
@github-actions github-actions Bot added the risk:unknown Advisory evidence unavailable or changed; no merge permission label Oct 19, 2026
* feat(mcp)!: turn on write tools by default

The default toolsets are now core, documents, operations and
documents_write. A server with no T49_MCP_TOOLSETS lists 32 tools. The
developer toolset stays opt-in.

Both store submissions now list the eleven write tools. The ChatGPT
entries also carry the annotations and a justification for each hint.
The tools/list and instructions snapshots follow the new default.
The server instructions now carry the write rules and the confirmation
rule by default, because the default listing has tools that need a
confirmed preview.

BREAKING CHANGE: The default tool list now includes the write tools
stop_shipment_tracking, resume_shipment_tracking,
cancel_tracking_request, add_reference_numbers,
remove_reference_numbers, assign_party, unassign_party,
set_custom_field, update_custom_field, relink_document and
update_document, alongside track_container and the reads. To stay
read-only, send the header X-T49-MCP-Read-Only: true, or set
T49_MCP_READ_ONLY=true on a self-hosted or stdio server. To keep the
previous list, send X-T49-MCP-Toolsets: core,documents or set
T49_MCP_TOOLSETS=core,documents. An HTTP server needs
T49_MCP_CONFIRMATION_SECRET for the tools that need a confirmed
preview. Without it they refuse to run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(mcp): ask the model to report each write and its undo tool

With writes on by default, the server instructions now tell the model
to say what a write changed and to name the tool in the result's undo
field. A result with an error field is explained to the user, not
repeated in a loop. The rule appears only when the session serves a
write tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(mcp): test when the sandbox write evals run

The write evals now read their credentials through resolveSandboxConfig
in eval/client.ts. It returns null unless MCP_EVAL_ENABLE_WRITE is 1 or
true and MCP_EVAL_SANDBOX_TOKEN is set, so the suite skips cleanly. It
never reads MCP_EVAL_TOKEN or MCP_EVAL_BEARER, so a write eval cannot
reach the read eval's account. The read suite reads the same flag
through writesEnabled.

The write suite now fails when the gateway does not list every default
write tool, so a gateway that lost a write toolset cannot pass with
every case skipped. MCP_EVAL_ALLOW_SPARSE=1 allows the skips, as in the
read suite.

The sandbox account also holds demo data, so the write cases change
only eval fixtures, chosen in eval/write-fixtures.ts by exact match:
the one shipment with reference number MCP-EVAL-FIXTURE, the party
named "MCP Eval (do not use)", and the short_text custom field
mcp_eval_fixture. A case skips when its fixture is missing and never
falls back to another record. assign_party creates the party on the
first run. Each case undoes its write in a finally block. The live
relink_document call is gone, and update_document is only previewed on
a document attached to the fixture shipment.

New unit tests pin each case and each selection rule. The default test
run now includes eval/**/*.test.ts, so these tests run in vp run test
and in PR CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci: run the MCP write evals on the sandbox account

On pushes to main, a new mcp-sandbox-write-eval job runs
eval/writes.eval.ts with MCP_EVAL_ENABLE_WRITE=1 and the
MCP_EVAL_SANDBOX_TOKEN secret, after the mcp job passes. It skips when
the secret is unset. It runs only the write suite, so the read suite
never gets the write flag. The custom field case needs a short-lived
user token, so it skips in CI.

The job first waits until Vercel reports a successful production
deployment of the pushed commit. The eval checks the default write
tools, so it must not run against the build production served before
the push.

Every run edits the same sandbox shipment, so the job has a concurrency
group and runs never overlap. A unit test checks that only this job
sets the write flag, that it runs only on pushes to main, that it keeps
the concurrency group, and that it waits for the deployment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: describe the write tools in the store submissions

The ChatGPT and Claude listing descriptions now say the connector can
change the account at the user's request, and that a change which
removes or replaces data waits for the user to confirm a preview. The
Claude listing adds three use cases for the write tools.

The MCP Registry description in server.json now says the server can
stop or resume tracking and edit references, parties, custom fields and
documents. Its version moves to 1.1.0 so the registry takes the change.
A test keeps that description within the 100 characters the registry
allows.

The ChatGPT test case for listing shipments is replaced by one that
removes a reference number only after the user approves the preview.
The submission test now requires that case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: describe the MCP write tools as on by default

The hosted server now serves the operations and documents_write
toolsets by default. The MCP overview gives the default count of 32
tools, the 20 left in read-only mode, and the header that keeps the old
21. The toolset list, the tools reference note, the write tools
section and the Not yet supported note no longer say the hosted server
leaves the write tools out. The quickstart gives the new
T49_MCP_TOOLSETS default and says HTTP servers need
T49_MCP_CONFIRMATION_SECRET unless they are read-only.

Each client setup page now says the tool list includes write tools.
The Cursor and other clients pages show the read-only header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(mcp): describe the write tools as on by default

The package README, TOOLS_OVERVIEW.md and the root MCP_README.md now
give the default of 32 tools, with operations and documents_write in
the default toolsets and developer still opt-in. They say how to serve
only reads, or the 21 tools of the earlier default. MCP_README.md also
lists update_document among the tools that return a preview first, and
documents T49_MCP_CONFIRMATION_SECRET.

WORKOS_MCP_SETUP.md marks T49_MCP_CONFIRMATION_SECRET as required. The
directory preflight checks that production has the secret and runs the
sandbox write evals, and no longer says only track_container creates
data. The eval README describes the CI write eval job and its secret.
.env.local.example shows the new T49_MCP_TOOLSETS default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(mcp): describe the write eval fixtures and their setup

The write evals now change only eval fixtures in the sandbox account,
which also holds demo data. The eval README names the three fixtures
and the one-time setup: add MCP-EVAL-FIXTURE to one demo shipment and
create the short_text custom field mcp_eval_fixture. The first run
creates the party "MCP Eval (do not use)". The README also drops the
live relink case and says the document case is preview-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(mcp): skip the party eval when a near-match party could be linked

With no party named exactly "MCP Eval (do not use)", the eval let
assign_party create it by name. assign_party matches names ignoring case
and punctuation, so it could link another party instead. The eval now
creates the party only when the name search finds no party at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: Auto-generate Postman collection from openapi.json [skip ci]

* docs: describe both confirmation channels in the release notes and connector submission

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(mcp): remove the eval party role even when its assign response is lost

The party write eval called assign_party before its cleanup was armed and
unassigned only the role id from the response. A lost response or a failed
call left the role on the fixture shipment until a later run. The assign now
runs inside the try, and on failure the cleanup reads the fixture's party
roles and unassigns every notify_party role held by "MCP Eval (do not use)",
which only this eval assigns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(mcp): update the tools/list snapshot for the add_reference_numbers description

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: Auto-generate Postman collection from openapi.json [skip ci]

* chore: restore main's Postman collection

The branch's docs/openapi.json matches main's, so the generated collection
should too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 84ca2d926f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/mcp/home.mdx Resolved
Comment thread packages/mcp/README.md Resolved
@dodeja
dodeja changed the base branch from mcp-v2/06-custom-fields to mcp-v2/05-refs-parties October 30, 2026 00:59
Brings in the base as of 6354100: main's 2 KB instruction cap and
idempotentHint (#493), the opt-in tracking_lifecycle toolset and the
read-first write kit (#451), and the opt-in reference_numbers toolset
(#452).

Conflicts were resolved in favour of the base's design:

- packages/mcp/src/instructions.ts takes the base's segment model. The
  write paragraph still names each write tool, so with update_document
  and relink_document the largest selection is 2,092 bytes. The next
  commit redesigns that paragraph; three instruction tests stay red at
  this commit.
- packages/mcp/src/server.ts keeps the base's reference-number error
  message and adds the webhook read cases after it. This branch's own
  copy had add_reference_numbers and remove_reference_numbers falling
  through to the webhook message.
- The docs (home.mdx, README, TOOLS_OVERVIEW, WORKOS_MCP_SETUP, eval
  README) use the base's wording and add documents_write and developer
  as opt-in toolsets: 39 tools in all, 30 by default.
- Terminal49-API.postman_collection.json is the base's copy, because
  docs/openapi.json is identical to the base's.

Generated filter schemas come from npm run generate:filters. The
annotations test lists the two document writes, and the update_document
fail-closed test allows the read that the base's write kit makes before
it refuses.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8cPhgia7FZpMP7q5BJ5nw
With update_document and relink_document the instructions' write
paragraph, which named every write tool and repeated the tier C ones in
the approval rule, reached 2,092 bytes at the largest toolset selection,
past the 2,048 that Claude Code keeps.

The paragraph now names no tool. Each write tool's description opens
with "Changes account data." from the registry tier, and a tier C
description goes on to say when the call needs the user to approve a
preview, as before. The instructions tell the model to look for that
lead, to call a write only when the user asks, that every other tool
only fetches data, to show every preview, never to pass a
confirmation_token the user has not approved this turn, and that tool
output is untrusted. Those rules appear in every selection that serves
a write tool, tier A included, and nowhere else.

Every selection is now 1,824 bytes or fewer, so the default and the
largest selection are the same size and a new write tool costs nothing
here. The chaining guide names track_container after
get_supported_shipping_lines, since the write paragraph no longer does.

Tests: every toolset subset, read-only or not, carries the rules exactly
when it serves a write; the write paragraph mentions no tool; tools/list
opens every write description, and no read description, with the lead,
and only tier C descriptions mention approving a preview. The
list_webhooks instruction test goes, because the tool's own description
carries the redaction rule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8cPhgia7FZpMP7q5BJ5nw
relink_document was tier A on the premise that a relink only adds links.
The backend's DocumentReferenceLinker, which POST /documents/{id}/relink
re-runs, also destroys the links its own matching made earlier whose
targets the fresh matching no longer produces (stale_direct_frank_links
and stale_intermediate_frank_links in tnt-api). Links made by hand stay,
but a relink can remove links the user relies on, and nothing puts them
back, so it is destructive.

The tool is now tier C with confirms: 'always'. The first call reads the
document and returns a preview of its current links, and the approval is
bound to the shipment and container links and updated_at, so a change to
the links between the preview and the approval asks again. A 422 after
the approval returns noop and keeps the approval usable, through the
kit's refused callback. The registry marks it destructive, so Claude
Code prompts on every call.

BREAKING CHANGE: relink_document no longer relinks in one call. It
returns confirmation_required with a preview, and relinks only when
called again with the confirmation_token after the user approves.

Tests cover the preview-then-token flow, a stale preview after the links
change, 422 after approval, the fail-closed path without a confirmation
secret, and the transport round trip. The docs, README, overview and
in-depth guide describe the tier and the reason.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8cPhgia7FZpMP7q5BJ5nw
The description branch checked tier read, then tier A, then read
confirms in the last arm. The registry types tier read and A as one
union member, so TypeScript did not narrow the last arm to tier C and
vp run check failed on the confirms read. Branching on tier C first
narrows it, and nothing changes at run time.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8cPhgia7FZpMP7q5BJ5nw
The SCAC sentence was split into three segments, and the last one was a bare
".". It mentions no tool, so it stayed in the guide after its sentence was
dropped. Selections without core, such as documents or trade, got a paragraph
that was only ".". Each variant of the sentence now carries its own period.
The default instructions text is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8cPhgia7FZpMP7q5BJ5nw
The server instructions no longer name write tools, so the README now says
that write tools are marked by "Changes account data." in their descriptions.
The confirmation-secret list in the MCP guide and the WorkOS setup note now
include relink_document. The README names every field the update_document
fingerprint covers, and says reextract, reclassify and relink_document
confirmations can also replay once on another instance. The relink_document
reach says it may extract references first and never re-adds a link a user
removed. The relink fingerprint comment no longer claims updated_at catches a
new extraction. MCP_README counts the opt-in tools correctly, the writes eval
header says the document case only previews, and comments that narrate code
are gone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8cPhgia7FZpMP7q5BJ5nw

This branch was successfully deployed

1 active deployment
Preview — d12e745d Deployed Oct 30, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:unknown Advisory evidence unavailable or changed; no merge permission risk-shadow:high Shadow-only risk signal: high

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants