From 988275ee232280a6aba2499d936e3a7552811f69 Mon Sep 17 00:00:00 2001 From: WebSearching404 <40842147+WebSearching404@users.noreply.github.com> Date: Wed, 1 Jul 2026 19:35:46 -0600 Subject: [PATCH] fix: user search filters take a slug or 'me', not a numeric ID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit validateUserIdFilters (0b9dea4) enforced the inverse of BookStack's actual contract: SearchRunner::filterCreatedBy/filterUpdatedBy/ filterOwnedBy resolve the filter value by user SLUG ('me' = the token's own user) and silently skip the filter when nothing matches. So the guard rejected every valid form of these filters (slugs, 'me') while steering callers to numeric IDs — exactly the values BookStack ignores, returning ALL results presented as filtered. - validateUserIdFilters now rejects pure-numeric values (the silent fail-open input), including the previously-unguarded negated {!field:N} form, and accepts slugs/'me' - search + find_users tool descriptions and the README row now state the slug contract Verified against BookStack v26.05.1 source and live on a seeded instance: {created_by:me} and {created_by:} filter correctly; numeric IDs silently return unfiltered results. Co-Authored-By: Claude Fable 5 --- README.md | 2 +- src/bookstack-client.ts | 20 ++++++++++++-------- src/index.ts | 4 ++-- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e750251..ab6f39e 100644 --- a/README.md +++ b/README.md @@ -242,7 +242,7 @@ Both templates support `id` autocompletion: as you type, the server searches Boo | `get_shelves` / `get_shelf` | List or get shelf details | | `get_attachments` / `get_attachment` | List or get attachment details | | `get_comments` / `get_comment` | List or get page comments (BookStack v25.11+) | -| `find_users` | Look up BookStack users by name, email, or slug to resolve numeric user IDs for search filters | +| `find_users` | Look up BookStack users by name, email, or slug to resolve user slugs for `{created_by:X}`-style search filters | | `get_recycle_bin` | List items in the recycle bin | | `export_page` | Export page as HTML, PDF, Markdown, plaintext, or ZIP | | `export_book` | Export entire book | diff --git a/src/bookstack-client.ts b/src/bookstack-client.ts index dad5ada..57f68bb 100644 --- a/src/bookstack-client.ts +++ b/src/bookstack-client.ts @@ -12,17 +12,21 @@ function parseRetryAfter(value: unknown): number | null { return null; } -// BookStack's advanced search silently ignores {created_by:X}/{updated_by:X}/{owned_by:X} -// when X isn't a numeric user ID — the query falls back to unfiltered results, which is -// a confusing footgun. Reject the bad form up front and point the caller at find_users. +// BookStack resolves {created_by:X}/{updated_by:X}/{owned_by:X} by user SLUG, with 'me' +// as a shortcut for the token's own user, and silently SKIPS the filter when X matches no +// user's slug — the query falls back to unfiltered results, which is a confusing footgun +// (SearchRunner::filterCreatedBy et al. look users up by slug only). A numeric user ID is +// the classic way to hit it, so reject that form up front and point the caller at +// find_users for the slug. function validateUserIdFilters(query: string): void { - const re = /\{(created_by|updated_by|owned_by):([^}\s]+)\}/g; + const re = /\{(!?)(created_by|updated_by|owned_by):([^}\s]+)\}/g; for (const match of query.matchAll(re)) { - const [, field, value] = match; - if (!/^\d+$/.test(value)) { + const [, neg, field, value] = match; + if (/^\d+$/.test(value)) { throw new Error( - `Search filter {${field}:${value}} requires a numeric user ID, not "${value}". ` + - `Use the find_users tool to look up a user's ID by name, email, or slug.` + `Search filter {${neg}${field}:${value}} takes a user slug or 'me', not a numeric user ID — ` + + `BookStack resolves these filters by slug and silently returns unfiltered results for ` + + `unmatched values. Use the find_users tool to look up a user's slug by name or email.` ); } } diff --git a/src/index.ts b/src/index.ts index fbab8ff..38d9a7b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -189,7 +189,7 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS readTool( "search_content", { - description: "Search BookStack content. Supports advanced syntax like {type:page} or {book_id:5}. {created_by:X}/{updated_by:X}/{owned_by:X} need a numeric user ID — use find_users to resolve names.", + description: "Search BookStack content. Supports advanced syntax like {type:page} or {book_id:5}. {created_by:X}/{updated_by:X}/{owned_by:X} take a user slug or 'me' — use find_users to resolve names to slugs.", inputSchema: { query: z.string(), type: z.enum(["book", "page", "chapter", "bookshelf"]).optional(), @@ -567,7 +567,7 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS readTool( "find_users", { - description: "List users; filter by name/email/slug (partial match). Resolves names to numeric user IDs for {created_by:X}/{updated_by:X}/{owned_by:X} search filters. Requires admin token.", + description: "List users; filter by name/email/slug (partial match). Resolves names to user slugs for {created_by:X}/{updated_by:X}/{owned_by:X} search filters. Requires admin token.", inputSchema: { name: z.string().optional(), email: z.string().optional(),