Skip to content

fix(mcp): resolve {{LIBRECHAT_USER_ID}} in loadTools' MCP tool-loading path - #32

Closed
ForisKuang wants to merge 73 commits into
cBioPortal:v0.8.7-custom-v1from
ForisKuang:fix/mcp-tool-load-user-id
Closed

fix(mcp): resolve {{LIBRECHAT_USER_ID}} in loadTools' MCP tool-loading path#32
ForisKuang wants to merge 73 commits into
cBioPortal:v0.8.7-custom-v1from
ForisKuang:fix/mcp-tool-load-user-id

Conversation

@ForisKuang

Copy link
Copy Markdown
Collaborator

Summary

PRs #29, #30, and #31 progressively fixed {{LIBRECHAT_USER_ID}} resolution across the createSafeUser helper, the MCP tool-call runtime path, and the agent-client config-building sites — but one call site was missed: loadTools() in api/app/clients/tools/util/handleTools.js.

That function receives the caller's already-resolved user ID as its own user parameter (the same value used two lines later for loadAuthValues({ userId: user, ... })), but its MCP-tool-loading section builds the user object handed to createMCPTools/createMCPTool from options.req?.user alone:

const safeUser = createSafeUser(options.req?.user);

When options.req.user is a plain object deserialized from the passport session (no id virtual, no _id) — the same class of object PR #29 addressed — safeUser.id stays unset and {{LIBRECHAT_USER_ID}} ships unresolved to the MCP server's x-user-id header, exactly like the bug #30/#31 fixed elsewhere.

Confirmed live in production, well after #31 deployed: service:cbioportal-mcp spans on the cbioagent Datadog dashboard still show enduser.id="{{LIBRECHAT_USER_ID}}" for real tool calls (clickhouse_run_select_query, read_guide, etc.) routed through this path.

Fix

Thread the existing user param through as createSafeUser's fallbackId, matching the pattern #31 established at every other call site:

const safeUser = createSafeUser(options.req?.user, user);

Test plan

  • Added regression tests in handleTools.test.js exercising the MCP 'all'-tools branch (the one that calls createMCPTools): one confirming the fallback resolves the id when req.user has neither id nor _id, one confirming an explicit req.user.id still takes precedence
  • Verified the new test fails without the fix (Received: {"user": {"email": ..., "provider": "local"}} — no id key) and passes with it
  • Full handleTools.test.js suite passes (14/14)
  • eslint clean on both changed files

🤖 Generated with Claude Code

inodb and others added 30 commits March 6, 2026 17:41
- Pre-create required runtime directories (/app/api/logs, /app/uploads, /app/client/public/images)
- Set proper ownership to node user for /app directory
- Run container as non-root user (USER node)

Fixes EACCES permission denied errors when LibreChat tries to create
the logs directory at runtime. Makes multi-stage build compatible with
docker-compose user directive.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add dompurify (v3.2.6) dependency for HTML sanitization
- Update Landing.tsx to render descriptions as sanitized HTML
- Convert agent description input to textarea for multi-line support
- Maintain 512 character limit with improved UX

Changes enable rich HTML formatting in agent descriptions while ensuring
security through DOMPurify sanitization.

Source: danny-avila#8291

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Uses vite-plugin-pwa's selfDestroying option which generates a special
service worker that unregisters itself and clears all caches. This
fixes stale cache issues causing blank pages after deployments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the product feedback feature is enabled (via FEEDBACK_ISSUE_SERVICE_URL
env var), clicking thumbs-down on an assistant message opens a feedback
modal instead of the default tag popover. The modal collects a categorized
issue report that is submitted to an external issue creation service,
enabling automated GitHub issue creation from user feedback.
…sues

Instead of proxying to an external service, the feedback route now calls
the GitHub API directly to create issues. This eliminates the need for a
separate middleware service. Configuration uses GITHUB_FEEDBACK_TOKEN and
GITHUB_FEEDBACK_REPO env vars. Issues are formatted with structured
markdown and labeled based on feedback reason.
…bmitted

The existing feedback storage endpoint was not being called when
submitting via the product feedback modal. Now handleFeedback is
invoked on modal submit to store the thumbs-down rating in MongoDB
alongside creating the GitHub issue.
…xt field

Instead of creating a separate ProductFeedback MongoDB collection,
serialize the entire feedback payload (reason, title, details,
suggested fix, conversation context) as a JSON string in the
existing message feedback `text` field. This avoids schema changes
while preserving all feedback data.
…d add agent routing

- Remove all GitHub API integration from feedbackIssues route; feedback
  is now stored in a new ProductFeedback MongoDB collection for manual review
- Add endpoint, model, and agent_id to the feedback payload so feedback
  can be attributed to the specific agent it was submitted against
- Thread agent identity from conversation through HoverButtons → Feedback
  → ProductFeedbackModal
- Change feature flag from GITHUB_FEEDBACK_TOKEN/GITHUB_FEEDBACK_REPO
  to a simple PRODUCT_FEEDBACK_ENABLED boolean env var
- Update response types from issue_url/issue_number to id/request_id
- Simplify success toast to a plain confirmation (no GitHub link)
- Make thumbs up/down feedback buttons amber/yellow for better visibility
- Remove edit and fork buttons from message hover actions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When agent responses use the content array format (with think/text
blocks), msg.text is empty. Fall back to extracting text from
msg.content to capture the actual response in feedback context.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix thumbs-down bug where both thumbs stayed visible
- Remove title field from thumbs-down modal, make categories optional/multi-select
- Add dynamic placeholder text based on selected feedback category
- Rename "Suggested Fix" to "Expected Outcome"
- Add "Suggested System Prompt" field for power users
- Add thumbs-up modal with category pills and optional comment
- Green thumbs-up / red thumbs-down colors, move feedback to last position
- Replace agent dropdown with horizontal button selector
- Add powerUserEmails config and POWER_USER_EMAILS env var
- Add GitHub Actions workflow for multi-arch Docker builds

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
K8s nodes are all arm64, no need for amd64 images.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The buttons were only updating local state without triggering the actual
agent/conversation switch. Now uses the context's handleSelectSpec which
calls onSelectSpec → newConversation. Also fix layout to inline-flex.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Agent buttons only show on new conversations, disappear once started
- Use spec.name (e.g. "cBioDBAgent") instead of full label description

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use ubuntu-24.04-arm for native arm64 builds instead of QEMU emulation.
Should cut build time from ~50min to ~10-15min.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enables filtering by agent in Langfuse (e.g. cBioDBAgent vs cBioNavigator).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a user submits thumbs up/down feedback, look up the corresponding
Langfuse trace by messageId and create a score (1 for thumbsUp, 0 for
thumbsDown) with the feedback comment. Fire-and-forget to avoid blocking
the response.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously, clicking thumbs down with productFeedbackEnabled would
immediately save the feedback before the modal was submitted, causing
stale state and the wrong modal to appear on re-click.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e and reason

The frontend no longer sends feedback_title (removed field) and sends
feedback_reason as optional comma-separated values. Updated backend
route and Mongoose schema to match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Clicking an active thumbs-down button now clears the feedback (same as
thumbs-up), instead of reopening the modal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of immediately clearing feedback when clicking the active thumb,
show a summary dialog displaying the stored tags/comment/details with
options to Delete (remove) or Update (re-open the appropriate modal).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace literal strings with i18n localize() calls and fix prettier
formatting. Also fix pre-existing Cancel/Submit literals in thumbs-up modal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Local state was set to { rating: 'thumbsDown' } without the text field,
so the summary dialog had no data to display. Now both local state and
parent callback receive the full feedback object including the JSON text.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The feedback mutation was invalidating the wrong cache key
([QueryKeys.messages, messageId] instead of [QueryKeys.messages, conversationId]),
so the messages cache was never updated after feedback was given. When users
navigated away and returned, the cached messages had no feedback data, making
the thumbs buttons appear unclicked.

Fix: Use setQueryData to directly update the specific message's feedback field
in the messages cache after a successful feedback mutation.
toMinimalFeedback() required a tag to be present, returning undefined
when no tag was set. This caused the new feedback modals (which store
data in the text field as JSON rather than using tags) to send
{ feedback: undefined } to the API, clearing the feedback instead of
saving it.

- Make tag optional in feedbackSchema and toMinimalFeedback
- Handle undefined tag in React Query cache update
fix: Persist
   thumbs up/down feedback state across navigation
…data

Clicking an active feedback thumb now opens the editable submission modal
(pre-filled with existing data) instead of the read-only summary dialog.
Delete buttons inside the modals replace the summary dialog entirely.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Parse product feedback JSON in Langfuse scores to show human-readable
  reason/details instead of raw JSON
- Add ?? 'other' fallback in toMinimalFeedback for when tag is missing
- Relax toMinimalFeedback guard to only require rating (not tag)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
inodb and others added 24 commits June 9, 2026 14:04
…-cards-default-open

Show cBioAgent examples by default
Adds dd-trace as a root dependency so NODE_OPTIONS=--require dd-trace/init
can auto-instrument Anthropic/OpenAI SDK calls at pod startup, enabling
token and cost tracking in the Datadog LLM Observability dashboard.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
chore: update package-lock.json for dd-trace
…BioPortal#20)

* Resolve any Lucide icon by name for conversation-starter categories

Replace the hardcoded 6-name categoryIcons map with a Lucide namespace
lookup. Kebab/snake/PascalCase input all resolve to the matching
Lucide component (book-open → BookOpen, info → Info, etc.).

Bundle: lucide-react is namespace-imported (dynamic key lookup defeats
tree-shaking), so the full set lands in the bundle.

* unused-packages: whitelist dd-trace (loaded via NODE_OPTIONS at runtime)
PR cBioPortal#20 used `import * as LucideIcons from 'lucide-react'` to resolve any
icon by name. That defeated tree-shaking and pulled the entire library
into the http-client chunk alongside axios, where a circular-init order
manifested as `Cannot access 'r' before initialization` (the icon
factory `r` referenced before module init) — a blank page on
chat.cbioportal.org.

Switch to explicit named imports for a curated set of ~30 icons that
cover the practical category labels we use. The kebab/snake →
PascalCase resolver is preserved, just looking up in a literal registry
instead of the namespace object.

If a new icon is needed, add it to the named-import list.
…ioPortal#22)

The previous fix added `Map` and `Navigation` as plain named imports
from lucide-react. Both shadow JS globals at module scope, and the
unrelated `useGetAssistantDocsQuery({ select: data => new Map(...) })`
call in the same file ended up invoking lucide's `Map` icon (a
forwardRef) as a constructor — production crashed with
`TypeError: pa is not a constructor` from React Router.

Alias them as MapIcon / NavigationIcon so the global `Map` constructor
stays reachable. Category-icon resolution still works via the
PascalCase registry key ("Map" → MapIcon).
…ortal#23)

Replace the curated 30-icon named-import registry with the official
dynamic loader. Each category icon is fetched as its own async chunk
only when first rendered, keyed by the kebab-case Lucide name
("map-pin", "heart-pulse", "stethoscope", …).

- No more hand-maintaining a list of allowed icons in code; configs
  can use any Lucide name.
- No risk of namespace shadowing — icons never appear at module scope.
- lucide-react no longer lands in the http-client / index chunks; each
  used icon is its own ~1KB lazy chunk, downloaded once per visit.

Input is normalized to kebab via a simple regex (`BookOpen` →
`book-open`, `book_open` → `book-open`, `Book Open` → `book-open`).
Unknown names fall back to Search; the Suspense fallback also renders
Search while a chunk loads.
Co-authored-by: ForisKuang <ForisKuang@users.noreply.github.com>
Co-authored-by: Ino de Bruijn <ino@ino.bio>
* Brand the SPA shell as cBioChat with the cbioportal icon

Replace the static <title>, meta description, and favicon set so the
HTML shell — what Google's crawler and link-preview cards see — reads
"cBioChat" with the cbioportal cell icon instead of LibreChat. The
runtime APP_TITLE env continues to drive in-app strings (page title
after first render, login alt text, etc.) and can override this per
deployment if needed.

Favicon source: src/rootImages/cbioportal_icon.png from
cbioportal-frontend (32x32, the official site favicon). Generated:
- favicon-16x16.png  (Lanczos downscale)
- favicon-32x32.png  (passthrough)
- apple-touch-icon-180x180.png (Lanczos upscale)
- icon-192x192.png             (Lanczos upscale)
- maskable-icon.png            (192x192 with PWA 80% safe-zone padding)

Upscaling a 32x32 source is inherently soft; cbioportal's brand assets
don't ship a higher-resolution raster of the icon-only mark. Acceptable
trade-off versus shipping LibreChat's default branding.

* Regenerate favicons from the cBioPortal org GitHub avatar (74x74)

The earlier set was upscaled from cbioportal-frontend's 32x32 raster,
which was visibly soft at 180/192. The cBioPortal GitHub org avatar
is 74x74 — still small but a 2.6x upscale to 192x192 instead of 6x,
notably crisper.

Source: https://avatars.githubusercontent.com/u/9876251?v=4

If a higher-resolution raster of the icon-only mark turns up, swap
the source and rerun the same generator.

* Maskable icon: keep 512x512 to match the webmanifest declaration

vite.config.ts declares this asset as sizes:"512x512" purpose:"maskable".
The earlier commit shrank it to 192x192 alongside the regular icon set,
which would fail PWA validators (Lighthouse / Chrome DevTools "PWA"
audit) and could cause Android home-screen installs to fall back to
the non-maskable icon.

Rebuild at 512x512 from the 74x74 GitHub avatar source, with the
content placed inside the inner 80% safe zone (~410x410) and 20%
padding around it so the OS's home-screen mask (circle, squircle,
rounded square, …) doesn't clip the icon.

* Maskable icon: opaque white background

A transparent canvas leaks through whatever shape mask the OS applies
on Android home-screen install (circle, squircle, rounded square, …),
producing ragged edges or background color showing through. The
maskable spec assumes the icon is opaque — the OS draws the shape
*over* the pixels rather than under them.

Fill the 512x512 canvas solid white before pasting the icon. The
icon's own background is already white, so the safe-zone padding now
visually matches the inner content instead of being see-through.

* Apple-touch icon: add safe-zone padding and opaque white background

iOS clips apple-touch-icons into a rounded square (~20% corner radius)
when added to the home screen, and the surface should be opaque (iOS
doesn't show through transparent regions). Without padding the
cbioportal cells were getting clipped at the corners.

Same treatment as the maskable icon: inner content at 80% (144x144 of
180x180), centered, on a solid white canvas. Visually consistent with
LibreChat's original — feather inside a circle with similar margin.

* icon-192: match the safe-zone padding + opaque white treatment

Keep the same shape language across the PWA icon set — inner 80%
content on a solid white canvas. Matches LibreChat's original
(feather-in-circle with padding) and stays visually consistent with
the apple-touch and maskable icons.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ggle

- Remove unreachable ExecuteCode, CodeAnalyze, and RetrievalCall returns
  in Part.tsx — the early-return block (toggle OFF) and the
  showReasoningActions guard (toggle ON) made them both inaccessible
- Drop the now-unused ExecuteCode, CodeAnalyze, and RetrievalCall imports
- Exclude agent-transfer tool calls (LC_TRANSFER_TO_*) from the toggle
  hide block so AgentHandoff always renders, matching its pre-toggle
  behaviour as a user-facing navigation event
- Remove tautological {showReasoningActions && <ToolCall>} in the
  web_search branch — that branch is only reachable when the toggle is
  already ON
- Initialize prevShowInfoRef to showReasoningActions (not false) in
  ToolCall.tsx to prevent a spurious expand animation on mount when the
  toggle is ON from a persisted localStorage value

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ns-toggle

feat: Add reasoning & actions visibility toggle
…serialized users

When `req.user` is a plain object deserialized from a Passport session
(rather than a live Mongoose document), the `id` virtual getter is absent
from the prototype chain. `createSafeUser` previously skipped the `id`
field entirely in that case, leaving `{{LIBRECHAT_USER_ID}}` unresolved in
MCP server headers and preventing `usr.id` from appearing in Datadog traces.

Add a fallback that derives `id` from `_id` whenever the `id` field isn't
captured via the `in` operator. Also adds `createSafeUser` unit tests,
including an end-to-end test that verifies `{{LIBRECHAT_USER_ID}}` resolves
through `processMCPEnv` when `id` comes from the `_id` fallback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…er-propagation

fix: ensure user id propagates to MCP request headers from session-deserialized users
… path (cBioPortal#30)

PR cBioPortal#29 fixed createSafeUser to populate `id` from `_id` for session-
deserialized users, but the MCP tool-call runtime path
(MCP.js -> MCPManager.callTool -> processMCPEnv) passes the raw
config.configurable.user and never calls createSafeUser. The placeholder
resolver read user.id only, so those users shipped the literal
"{{LIBRECHAT_USER_ID}}" header (confirmed in prod Datadog spans as
enduser.id="{{LIBRECHAT_USER_ID}}").

Fall back to `_id` inside processUserPlaceholders so id resolves for every
caller without needing createSafeUser. Adds regression tests exercising
processMCPEnv directly with a session-shaped user (no createSafeUser).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…/_id (cBioPortal#31)

PRs cBioPortal#29 and cBioPortal#30 populate the MCP x-user-id header from req.user.id / req.user._id,
but in the agent->MCP flow config.configurable.user = createSafeUser(req.user) and
req.user carries neither the id virtual nor _id. The user's ObjectId IS available
as the agent client's this.user (and userId in openai/responses), so
{{LIBRECHAT_USER_ID}} still shipped unresolved (confirmed in prod: enduser.id=
"{{LIBRECHAT_USER_ID}}" with both prod+beta already running cBioPortal#30).

Add an optional fallbackId to createSafeUser and pass this.user / userId at the
config-building sites so the header resolves regardless of req.user's shape.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…g path

PRs cBioPortal#29-cBioPortal#31 fixed user-id propagation into MCP headers across the agent
client, openai controller, and responses controller, but loadTools()
(api/app/clients/tools/util/handleTools.js) builds the MCP `user` object
from `options.req?.user` alone via createSafeUser. When req.user is a
plain object deserialized from the passport session (no `id` virtual, no
`_id`), that resolves to no usable id — even though loadTools is already
given the caller's resolved id via its own `user` param (ToolService.js
calls loadTools({ user: req.user.id, ... }), and that same `user` value is
already used directly for auth lookups a few lines up). Confirmed live in
production: mcp.tool/* spans on cbioportal-mcp still shipped
enduser.id="{{LIBRECHAT_USER_ID}}" well after PR cBioPortal#31 deployed.

Thread the existing `user` param through as createSafeUser's fallbackId,
matching the pattern PR cBioPortal#31 established elsewhere. Adds regression tests
exercising the MCP 'all'-tools branch (the one that calls createMCPTools),
verifying the fallback fires when req.user has neither id nor _id, and
that an explicit req.user.id still takes precedence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@inodb
inodb changed the base branch from v0.8.3-rc1-custom-v9 to v0.8.7-custom-v1 August 7, 2026 20:55
@inodb

inodb commented Aug 7, 2026

Copy link
Copy Markdown
Member

superseded by: #33

@inodb inodb closed this Aug 7, 2026
inodb added a commit that referenced this pull request Aug 7, 2026
Second follow-up to the v0.8.7 rebase. This test asserted the pre-fork
behavior ("we only check id") — but our fork intentionally treats _id
as a fallback for id via the createSafeUser + loadTools chain from PRs
#29/#30/#31 and #32 (the fix this PR ports). The whole point of that
chain is that passport-deserialized session user objects carry _id but
not id, and MCP tool calls previously shipped the literal placeholder
{{LIBRECHAT_USER_ID}} instead of the resolved id.

Update the "_id only" assertion to reflect what our fork actually
does. The paired "id takes precedence when both are present" assertion
below is already correct and unchanged.
inodb added a commit that referenced this pull request Aug 7, 2026
…g path (v0.8.7 port) (#33)

* fix(mcp): resolve {{LIBRECHAT_USER_ID}} in loadTools' MCP tool-loading path

PRs #29-#31 fixed user-id propagation into MCP headers across the agent
client, openai controller, and responses controller, but loadTools()
(api/app/clients/tools/util/handleTools.js) builds the MCP `user` object
from `options.req?.user` alone via createSafeUser. When req.user is a
plain object deserialized from the passport session (no `id` virtual, no
`_id`), that resolves to no usable id — even though loadTools is already
given the caller's resolved id via its own `user` param (ToolService.js
calls loadTools({ user: req.user.id, ... }), and that same `user` value is
already used directly for auth lookups a few lines up). Confirmed live in
production: mcp.tool/* spans on cbioportal-mcp still shipped
enduser.id="{{LIBRECHAT_USER_ID}}" well after PR #31 deployed.

Thread the existing `user` param through as createSafeUser's fallbackId,
matching the pattern PR #31 established elsewhere. Adds regression tests
exercising the MCP 'all'-tools branch (the one that calls createMCPTools),
verifying the fallback fires when req.user has neither id nor _id, and
that an explicit req.user.id still takes precedence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Include conversation_starters in the VIEW safe-list test expectation

Follow-up to the v0.8.7 rebase: our fork intentionally exposes
`conversation_starters` on the list-agents endpoint via commit b044fe8
("Fix agent conversation starters not returned by list endpoint") so
that the landing page can render them without a second per-agent fetch.

Upstream added a "mass assignment protection" security test that asserts
the exact safe-list of returned fields — that test doesn't know about
our extension and fails on any PR against v0.8.7-custom-v1. Rather than
undo b044fe8's projection change (which would break the landing UI),
extend the test's expected list to reflect what our fork intentionally
returns.

Distinct from the mcp user-id fix that opened this PR, but folded in
so v0.8.7-custom-v1 gets a clean CI baseline.

* mcp.spec: expect _id to resolve LIBRECHAT_USER_ID (fork behavior)

Second follow-up to the v0.8.7 rebase. This test asserted the pre-fork
behavior ("we only check id") — but our fork intentionally treats _id
as a fallback for id via the createSafeUser + loadTools chain from PRs
#29/#30/#31 and #32 (the fix this PR ports). The whole point of that
chain is that passport-deserialized session user objects carry _id but
not id, and MCP tool calls previously shipped the literal placeholder
{{LIBRECHAT_USER_ID}} instead of the resolved id.

Update the "_id only" assertion to reflect what our fork actually
does. The paired "id takes precedence when both are present" assertion
below is already correct and unchanged.

---------

Co-authored-by: ForisKuang <ForisKuang@users.noreply.github.com>
Co-authored-by: Claude Sonnet 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.

3 participants