Skip to content

Fix: every tool call fails in multi-base mode with two or more bases - #5

Merged
christophdb merged 1 commit into
seatable:mainfrom
burk3rt:fix/multi-base-log-base-info
Sep 4, 2026
Merged

christophdb merged 1 commit into
seatable:mainfrom
burk3rt:fix/multi-base-log-base-info

Conversation

@burk3rt

@burk3rt burk3rt commented Sep 3, 2026

Copy link
Copy Markdown

Symptom

With two or more entries in SEATABLE_BASES, every tools/call fails with:

MCP error -32603: Multiple bases available (requirements, concept). Specify "base" parameter.

even when a valid base argument is supplied. With exactly one entry in SEATABLE_BASES the identical call works.

Root cause

handleCallTool() runs the tool handler inside contextualClient.runWithBase(...), but reads this.client.getBaseInfo() for the log line after that scope has returned (src/mcp/server.ts, success path and catch path). At that point the AsyncLocalStorage store is undefined, so ContextualClient's client getter calls registry.resolve(undefined) — which only has a defaultName when exactly one base is configured and otherwise throws Multiple bases available. The failure is in logging after the work, not in the data path, which is why it is invisible with a single base and why it discards a result that was already correct.

Evidence

Reproduced against main (1.6.1) over stdio with a fake SeaTable API (app-access-token + metadata endpoints only), calling list_tables with { base: "<first base>" }:

SEATABLE_BASES outcome token requests metadata requests
1 base success 1 1
2 bases (before fix) Multiple bases available (…) 1 1
2 bases (after fix) success 1 1

The two-base run fetches its data exactly once and gets it — the handler succeeded and the answer was thrown away afterwards.

Fix

Capture the base info inside the runWithBase scope (in a finally around the handler, so it covers the error path too) and use that value in both log lines. Both sites needed it: without the catch-path fix, real errors in multi-base mode still surface as this misleading message instead of the actual failure.

The capture is defensively wrapped so a genuinely unresolvable base (missing or unknown base argument) still yields the original error rather than one from logging; when the base does resolve, dtable_uuid and app_name are logged as before.

Tests

Two regression tests in tests/server.spec.ts using a two-base ContextualClient: a tool call succeeds, and the completion log line carries the resolved base's dtable_uuid / app_name. Both fail on main and pass with this change.

npm test (315 tests), npm run lint and npm run typecheck pass. npm run format reports pre-existing style warnings across the repo (including these two files before this change); no reformatting was done.

Single-base mode is unaffected — resolve(undefined) still returns the default client there.

handleCallTool() read getBaseInfo() after runWithBase() had returned, so
the AsyncLocalStorage store was already unset. With two or more bases
ClientRegistry.resolve(undefined) has no default and throws "Multiple
bases available", turning every successful tool call into an error.

Capture the base info inside the handler scope instead, and use it in
both the success and the error log line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TXnsxX58RpvMWysyjqjrKJ
@christophdb
christophdb merged commit 6c12b8a into seatable:main Sep 4, 2026
4 checks passed
christophdb added a commit that referenced this pull request Sep 4, 2026
)

#5 fixed the one place that read the client after runWithBase() had returned.
This makes the next such mistake announce itself instead of hiding.

ContextualClient resolved through registry.resolve(getStore()), which could not
tell "no scope at all" from "in a scope, no base named" — both arrived as
undefined. With a single base the out-of-scope read then quietly succeeded via
the registry default; with two or more it threw `Multiple bases available …
Specify "base" parameter`, blaming the caller for omitting an argument they had
supplied. That combination is what let the defect survive five months: the
configuration that would have exposed it is not the one we run.

The store now carries { base } instead of the bare name, so an absent store is
detectable, and reading outside the scope throws a message that names the
actual error — the same way regardless of how many bases are configured.


Claude-Session: https://claude.ai/code/session_01Tn4FT2sVqoFz1QdpNDNAy7

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@christophdb

Copy link
Copy Markdown
Member

Danke für den PR — Analyse, Repro-Tabelle und Regressionstests, das war so gut wie fertig zum Mergen.

Ich habe den Bug unabhängig nachgestellt und kann bestätigen, dass es exakt so war wie beschrieben: mit zwei Bases fallen alle 22 Tools um, list_bases und ping_seatable inklusive, weil sie durch denselben Logging-Pfad laufen. Auch dein zweiter Punkt stimmt — ohne den catch-Fix wurde aus einem Tippfehler im Base-Namen die irreführende Meldung "Specify base parameter" statt "Unknown base".

Ist als 1.6.4 released. Aufbauend darauf wirft ContextualClient jetzt einen eigenen Fehler, wenn er außerhalb von runWithBase() benutzt wird (#9) — im Single-Base-Fall lief das vorher still durch, was der Grund war, warum das fünf Monate niemandem auffiel.

Entschuldige die Verzögerung: bei Fork-PRs müssen wir die Workflows manuell freigeben, das ist untergegangen.

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.

2 participants