Skip to content

fix: keep canAccess tools visible to sessions without auth after a runtime change - #389

Merged
punkpeye merged 1 commit into
punkpeye:mainfrom
fadiroot:fix/tools-list-changed-no-auth
Sep 24, 2026
Merged

punkpeye merged 1 commit into
punkpeye:mainfrom
fadiroot:fix/tools-list-changed-no-auth

Conversation

@fadiroot

Copy link
Copy Markdown
Contributor

Problem

Session creation only consults canAccess when the session has an auth object. With no authenticate configured (or none passed to connect()), every tool is visible; the existing test "canAccess works without authentication" and the README example canAccess: (auth) => auth?.role === "admin" rely on that.

FastMCPSession.toolsListChanged() did not apply the same rule. It called canAccess(undefined) on every tool, so:

  • after any runtime addTool() / removeTool(), a tool with canAccess: (auth) => auth?.role === "admin" disappeared from tools/list for unauthenticated sessions, and tools/call on it answered "Unknown tool";
  • a canAccess written without optional chaining (auth => auth.role === "admin") threw a TypeError out of server.addTool(), after #tools had already been updated, and skipped the refresh for every later session in the loop.

Repro:

const server = new FastMCP<{ role: string }>({ name: "T", version: "1.0.0" });
server.addTool({ name: "admin", canAccess: (a) => a?.role === "admin", execute: async () => "x" });
// connect an in-memory client with no auth
(await client.listTools()).tools.map((t) => t.name); // ["admin"]
server.addTool({ name: "public", execute: async () => "y" });
await new Promise((r) => setTimeout(r, 0));
(await client.listTools()).tools.map((t) => t.name); // ["public"]  <- admin vanished

Fix

toolsListChanged() now uses the same rule as #createSession: filter with canAccess only when the session has auth.

Tests

Two cases added to src/FastMCP.can-access-empty.test.ts (in-memory transport, no auth): the admin-only tool stays listed and callable after a runtime addTool(), and an addTool() with a canAccess that dereferences auth does not throw. Both fail on main and pass with this change. prettier, eslint and tsc --noEmit are clean.

…ntime change

Session creation only consults `canAccess` when the session has an auth
object: with no `authenticate` configured, or none passed to `connect()`,
every tool is visible, which is what the "canAccess works without
authentication" test and the README examples rely on.

`FastMCPSession.toolsListChanged()` did not apply that rule. It called
`canAccess(undefined)` on every tool, so after any runtime `addTool()` or
`removeTool()` a tool such as `canAccess: (auth) => auth?.role === "admin"`
disappeared from `tools/list` for unauthenticated sessions, and a
`canAccess` written without optional chaining threw a TypeError out of
`addTool()`, skipping the refresh for every later session.

Use the same rule as `#createSession`: filter with `canAccess` only when
the session has auth.
@fadiroot

Copy link
Copy Markdown
Contributor Author

The OpenAPI (network) failure here is the Stripe spec hash drift (stripe.json does not match its pinned hash), not this change; #391 re-pins it. The Test job passes.

@punkpeye
punkpeye merged commit 91b6d8b into punkpeye:main Sep 24, 2026
1 of 2 checks passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 4.20.17 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants