victor HF Staff commited on
Commit
e67ab0e
·
unverified ·
1 Parent(s): a2feefd

MCP (#1981)

Browse files

* Update ServerCard.svelte

feat(mcp): add Model Context Protocol integration with tool support

Improve MCP tools caching and server selection logic

Replaces single-entry MCP tools cache with a multi-keyed cache based on server configuration, improving cache accuracy for different server sets. Refactors server selection logic in runMcpFlow to better handle custom server lists and selected server names. Removes MCP flow from generate.ts, streamlining text generation logic.

* Add OAuth authentication for MCP servers

Introduces browser-based OAuth flow for MCP servers, including new auth helpers, localStorage management, and UI actions for authentication and sign-out. Server cards now support authentication, and enabled servers include persisted auth headers for API requests. Adds callback handler and route for OAuth completion.

* Add server authentication status and UI indicators

Introduces an 'authRequired' property to MCPServer and health check responses to indicate when authentication is needed. Updates ServerCard UI to show authentication badges and conditionally display Authenticate/Sign out buttons. Store now tracks authenticated server IDs and updates them on auth changes. Health check logic and error handling improved to detect and signal authentication requirements.

* Show MCP Servers button only for authenticated users

The MCP Servers button in NavMenu is now conditionally rendered for users with a username or email, ensuring only authenticated users can access server management. Also, MCPServerManager modal width and help text background color have been updated for improved UI consistency.

* format

* Refactor MCP client management and tool invocation

Introduces a pooled MCP client system for efficient reuse and connection management. Refactors tool invocation to support per-call timeouts, abort signals, and parallel execution with improved error handling. Ensures MCP clients are properly closed after each flow and updates related modules to use the new client pool and signal-aware APIs.

* Improve OAuth state generation and error handling

Replaces Math.random-based OAuth state with a cryptographically-strong random generator using Web Crypto API. Sanitizes error messages in the callback handler to prevent unsafe HTML embedding. Ensures token_type is trimmed and defaults to 'Bearer' in authentication headers.

* Update blue color shades and improve MCP server UI

Standardizes blue color usage across components for consistency, switching from blue-500 to blue-600 in buttons, borders, and backgrounds. Refactors MCPServerManager and ServerCard to use a Switch component for enabling servers, improving accessibility and code clarity. Increases loop count in runMcpFlow.ts for more robust streaming. Updates multimodal model indicators to use blue-600 in dark mode for better visual alignment.

* Adjust input padding and card border styling

Reduced horizontal padding on AddServerForm input fields for improved layout. Updated ServerCard selected state to use semi-transparent blue border for better visual distinction.

* Improve provider metadata handling in text generation

Sanitizes tool/function names to comply with provider guidelines by replacing disallowed characters and limiting length. Enhances router metadata emission to support cases where only provider information is available, and captures the x-inference-provider header from upstream OpenAI-compatible servers to notify the UI of the provider used.

* Update ServerCard.svelte

* Add per-model tool calling support and overrides

Introduces a `supportsTools` flag for models to indicate tool/function calling capability, aggregates provider support, and adds per-model user overrides via settings. Updates UI to display tool support, enables forced tool calling, and ensures backend respects these settings during text generation and conversation flows.

* lint

* Enhance MCP client to append structuredContent

Updates the MCP HTTP client to append structuredContent as JSON to the textual output if provided by the server. Also bumps @modelcontextprotocol/sdk dependency to version 1.21.1.

* Refactor settings bindings for Svelte 5 compatibility

Replaces direct store mutations with functional bindings for settings fields in model and application settings pages, ensuring proper updates via store methods. Also updates ToolUpdate.svelte to use $derived.by for availableTools and initializes state variables with explicit undefined values.

* Update MCPServerManager.svelte

* Improve MCP server management UI and validation

Added a security warning to the AddServerForm and improved form styling. Updated MCPServerManager to clarify views, use a larger add icon, and show quick tips. ServerCard header styling was refined. MCP server store now prunes invalid selected server IDs after refresh.

* Add router tool support configuration

Introduces LLM_ROUTER_ENABLE_TOOLS environment variable and configures its usage in both .env and prod.yaml. Updates model processing to enable tool support for the router when the variable is set to true.

* Improve MCP server manager and card UI

* Refactor MCP icon and update app name usage

Extracted the MCP icon into a reusable IconMCP.svelte component and replaced inline SVG usage in MCPServerManager.svelte. Updated references to 'HuggingChat' to use the dynamic PUBLIC_APP_NAME from public config for better branding flexibility.

* Update model badges to use colored backgrounds

Replaces border-based styling for tool and multimodal badges with colored backgrounds and text for improved visual clarity in models and settings pages.

* Remove 'Base' label from ServerCard

Eliminates the conditional rendering of the 'Base' label for servers of type 'base' in the ServerCard component.

* Add router bypass for tool-capable models

Implements logic to bypass Arch routing and select a configured tool-capable model when tools are enabled and active, using new helpers in toolsRoute.ts. Updates environment and documentation to describe new LLM_ROUTER_TOOLS_MODEL config. Ensures fallback to Arch routing if no suitable model is found.

* Add <think> block handling for reasoning tokens

Introduces logic to merge provider-specific reasoning fields into <think> blocks within the token stream, mirroring OpenAI adapter behavior. Ensures <think> blocks are closed before final output and strips them from tool call messages to prevent confusion in follow-up reasoning.

* Update ChatInput.svelte

* Update ChatInput.svelte

* Update ChatInput.svelte

* Refactor formatting and fix indentation in server code

Improves code readability in toolsRoute.ts and runMcpFlow.ts by reformatting multi-line statements and correcting indentation. No functional changes were made.

* Handle user aborts quietly in runMcpFlow

* Remove namespaced tool aliases from OpenAI tools

* Update Switch.svelte

* Add navigation for tool update groups in chat

* Add MCP server favicon support and update config

* Fix router details rendering with missing metadata

Updates conditional rendering to check for the presence of 'route' in streamingRouterMetadata, preventing errors when metadata is undefined or missing the 'route' property.

* format

* Add model tool support indicator to chat UI

Introduces a `modelSupportsTools` prop to ChatInput and ChatWindow components to reflect whether the selected model supports tool calling. The MCP server indicator now visually changes and updates its tooltip based on tool support, improving user feedback for models without tool capabilities.

* Improve MCP health check URL validation and timeout

* Update server manager and card UI styles and labels

* Update prod.yaml

* Remove obsolete LLM log files

Deleted three outdated LLM log files related to chat completions for improved log management and reduced clutter.

* derive tool name in tool group

* Make MCP server count clickable to open manager

Replaces the MCP server count text in ChatInput with a button that opens the MCPServerManager. Also improves layout responsiveness in MCPServerManager for small screens.

* Update MCPServerManager.svelte

* Improve MCP server health check and tool response handling

* remove mcp oauth ...for now

* feat(chat): allow manual navigation of tool-call groups while streaming\n\n- Add toolAutoFollowLatest state to stop auto-snapping when user pages\n- Default to newest group while streaming; clamp index as groups change\n- Resume auto-follow when streaming ends or user returns to newest group\n\nAffects: src/lib/components/chat/ChatMessage.svelte

* feat(chat): show active tool call in footer and hide routing while calling\n\n- Derive current in-flight tool from assistant updates\n- Map to human-friendly displayName using page.data.tools\n- When a tool is actively running: render "Calling tool <name>" and skip the router/model status\n\nAffects: src/lib/components/chat/ChatWindow.svelte

* feat(mcp): optionally forward HF user token to official HF MCP endpoint\n\n- Add MCP_FORWARD_HF_USER_TOKEN config flag\n- Forward logged-in user's HF token to https://huggingface.co/mcp?login when no Authorization header is set\n- Apply overlay in runMcpFlow and in /api/mcp/health endpoint\n- Add shared helpers (hasAuthHeader, isStrictHfMcpLogin, hasNonEmptyToken)\n- Clarify client store comment about server-side overlay\n\nAffects: src/lib/server/config.ts, src/lib/server/textGeneration/mcp/runMcpFlow.ts, src/routes/api/mcp/health/+server.ts, src/lib/server/mcp/hf.ts, src/lib/stores/mcpServers.ts

* infra(chart): add read-mcp scope to OPENID_SCOPES for MCP access\n\n- Update dev and prod charts to request 'read-mcp' alongside 'openid profile inference-api'

* feat(prompt): include current date and image markdown hint in tool preprompt\n\n- Add today's date to tool preprompt for time-aware tools\n- Document how to inline generated images via markdown\n\nAffects: src/lib/server/textGeneration/utils/toolPrompt.ts

* chore(ui): minor formatting and cleanup in MCP ServerCard and conversation page import\n\n- Collapse named imports and remove stray blank lines in ServerCard\n- Normalize import whitespace in +page.svelte\n\nNo functional changes

* Update .env

* infra(chart): enable HF MCP t

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +12 -2
  2. .github/workflows/slugify.yaml +13 -13
  3. README.md +37 -0
  4. chart/env/dev.yaml +6 -1
  5. chart/env/prod.yaml +7 -2
  6. package-lock.json +934 -97
  7. package.json +1 -0
  8. src/hooks.server.ts +4 -0
  9. src/lib/components/NavMenu.svelte +23 -0
  10. src/lib/components/Switch.svelte +1 -1
  11. src/lib/components/chat/ChatInput.svelte +122 -2
  12. src/lib/components/chat/ChatMessage.svelte +60 -2
  13. src/lib/components/chat/ChatWindow.svelte +46 -2
  14. src/lib/components/chat/FileDropzone.svelte +1 -1
  15. src/lib/components/chat/MarkdownRenderer.svelte.test.ts +0 -54
  16. src/lib/components/chat/OpenReasoningResults.svelte +2 -2
  17. src/lib/components/chat/ToolUpdate.svelte +246 -0
  18. src/lib/components/icons/IconMCP.svelte +28 -0
  19. src/lib/components/mcp/AddServerForm.svelte +250 -0
  20. src/lib/components/mcp/MCPServerManager.svelte +185 -0
  21. src/lib/components/mcp/ServerCard.svelte +203 -0
  22. src/lib/server/api/routes/groups/models.ts +2 -0
  23. src/lib/server/api/routes/groups/user.ts +2 -2
  24. src/lib/server/config.ts +3 -1
  25. src/lib/server/endpoints/openai/endpointOai.ts +8 -13
  26. src/lib/server/endpoints/preprocessMessages.ts +19 -20
  27. src/lib/server/mcp/clientPool.ts +48 -0
  28. src/lib/server/mcp/hf.ts +21 -0
  29. src/lib/server/mcp/httpClient.ts +61 -0
  30. src/lib/server/mcp/registry.ts +76 -0
  31. src/lib/server/mcp/tools.ts +182 -0
  32. src/lib/server/models.ts +12 -0
  33. src/lib/server/router/endpoint.ts +49 -0
  34. src/lib/server/router/toolsRoute.ts +51 -0
  35. src/lib/server/textGeneration/generate.ts +177 -22
  36. src/lib/server/textGeneration/index.ts +30 -1
  37. src/lib/server/textGeneration/mcp/routerResolution.ts +105 -0
  38. src/lib/server/textGeneration/mcp/runMcpFlow.ts +554 -0
  39. src/lib/server/textGeneration/mcp/toolInvocation.ts +284 -0
  40. src/lib/server/textGeneration/reasoning.ts +23 -0
  41. src/lib/server/textGeneration/types.ts +2 -0
  42. src/lib/server/textGeneration/utils/routing.ts +21 -0
  43. src/lib/server/textGeneration/utils/toolPrompt.ts +15 -0
  44. src/lib/server/urlSafety.ts +25 -0
  45. src/lib/stores/mcpServers.ts +247 -0
  46. src/lib/stores/settings.ts +1 -0
  47. src/lib/types/Message.ts +2 -0
  48. src/lib/types/MessageUpdate.ts +40 -0
  49. src/lib/types/Settings.ts +7 -0
  50. src/lib/types/Tool.ts +74 -0
.env CHANGED
@@ -34,7 +34,7 @@ COUPLE_SESSION_WITH_COOKIE_NAME=
34
  # when OPEN_ID is configured, users are required to login after the welcome modal
35
  OPENID_CLIENT_ID=
36
  OPENID_CLIENT_SECRET=
37
- OPENID_SCOPES="openid profile inference-api"
38
  USE_USER_TOKEN=
39
  AUTOMATIC_LOGIN=# if true authentication is required on all routes
40
 
@@ -73,10 +73,15 @@ LLM_ROUTER_MAX_ASSISTANT_LENGTH=500
73
  LLM_ROUTER_MAX_PREV_USER_LENGTH=400
74
 
75
  # Enable router multimodal fallback (set to true to allow image inputs via router)
76
- LLM_ROUTER_ENABLE_MULTIMODAL=false
77
  # Optional: specific model to use for multimodal requests. If not set, uses first multimodal model
78
  LLM_ROUTER_MULTIMODAL_MODEL=
79
 
 
 
 
 
 
80
  # Router UI overrides (client-visible)
81
  # Public display name for the router entry in the model list. Defaults to "Omni".
82
  PUBLIC_LLM_ROUTER_DISPLAY_NAME=Omni
@@ -113,6 +118,11 @@ ADMIN_TOKEN=#We recommend leaving this empty, you can get the token from the ter
113
  LLM_SUMMARIZATION=true # generate conversation titles with LLMs
114
 
115
  ALLOW_IFRAME=true # Allow the app to be embedded in an iframe
 
 
 
 
 
116
  ENABLE_DATA_EXPORT=true
117
 
118
  ### Rate limits ###
 
34
  # when OPEN_ID is configured, users are required to login after the welcome modal
35
  OPENID_CLIENT_ID=
36
  OPENID_CLIENT_SECRET=
37
+ OPENID_SCOPES="openid profile inference-api read-mcp"
38
  USE_USER_TOKEN=
39
  AUTOMATIC_LOGIN=# if true authentication is required on all routes
40
 
 
73
  LLM_ROUTER_MAX_PREV_USER_LENGTH=400
74
 
75
  # Enable router multimodal fallback (set to true to allow image inputs via router)
76
+ LLM_ROUTER_ENABLE_MULTIMODAL=
77
  # Optional: specific model to use for multimodal requests. If not set, uses first multimodal model
78
  LLM_ROUTER_MULTIMODAL_MODEL=
79
 
80
+ # Enable router tool support (set to true to allow tool calling via router)
81
+ LLM_ROUTER_ENABLE_TOOLS=
82
+ # Required when tools are active: id or name of the model to use for MCP tool calls.
83
+ LLM_ROUTER_TOOLS_MODEL=
84
+
85
  # Router UI overrides (client-visible)
86
  # Public display name for the router entry in the model list. Defaults to "Omni".
87
  PUBLIC_LLM_ROUTER_DISPLAY_NAME=Omni
 
118
  LLM_SUMMARIZATION=true # generate conversation titles with LLMs
119
 
120
  ALLOW_IFRAME=true # Allow the app to be embedded in an iframe
121
+
122
+ # Base servers list (JSON array). Example: MCP_SERVERS=[{"name": "Web Search (Exa)", "url": "https://mcp.exa.ai/mcp"}, {"name": "Hugging Face", "url": "https://huggingface.co/mcp"}]
123
+ MCP_SERVERS=
124
+ # When true, forward the logged-in user's Hugging Face access token
125
+ MCP_FORWARD_HF_USER_TOKEN=
126
  ENABLE_DATA_EXPORT=true
127
 
128
  ### Rate limits ###
.github/workflows/slugify.yaml CHANGED
@@ -4,12 +4,12 @@ on:
4
  workflow_call:
5
  inputs:
6
  value:
7
- description: 'Value to slugify'
8
  required: true
9
  type: string
10
  outputs:
11
  slug:
12
- description: 'Slugified value'
13
  value: ${{ jobs.generate-slug.outputs.slug }}
14
 
15
  jobs:
@@ -22,7 +22,7 @@ jobs:
22
  - name: Setup Go
23
  uses: actions/setup-go@v5
24
  with:
25
- go-version: '1.21'
26
 
27
  - name: Generate slug
28
  id: slugify
@@ -30,43 +30,43 @@ jobs:
30
  # Create working directory
31
  mkdir -p $HOME/slugify
32
  cd $HOME/slugify
33
-
34
  # Create Go script
35
  cat > main.go << 'EOF'
36
  package main
37
-
38
  import (
39
  "fmt"
40
  "os"
41
  "github.com/gosimple/slug"
42
  )
43
-
44
  func main() {
45
  if len(os.Args) < 2 {
46
  fmt.Println("Usage: slugify <text>")
47
  os.Exit(1)
48
  }
49
-
50
  text := os.Args[1]
51
  slugged := slug.Make(text)
52
  fmt.Println(slugged)
53
  }
54
  EOF
55
-
56
  # Initialize module and install dependency
57
  go mod init slugify
58
  go mod tidy
59
  go get github.com/gosimple/slug
60
-
61
  # Build
62
  go build -o slugify main.go
63
-
64
  # Generate slug
65
  VALUE="${{ inputs.value }}"
66
  echo "Input value: $VALUE"
67
-
68
  SLUG=$(./slugify "$VALUE")
69
  echo "Generated slug: $SLUG"
70
-
71
  # Export
72
- echo "slug=$SLUG" >> $GITHUB_OUTPUT
 
4
  workflow_call:
5
  inputs:
6
  value:
7
+ description: "Value to slugify"
8
  required: true
9
  type: string
10
  outputs:
11
  slug:
12
+ description: "Slugified value"
13
  value: ${{ jobs.generate-slug.outputs.slug }}
14
 
15
  jobs:
 
22
  - name: Setup Go
23
  uses: actions/setup-go@v5
24
  with:
25
+ go-version: "1.21"
26
 
27
  - name: Generate slug
28
  id: slugify
 
30
  # Create working directory
31
  mkdir -p $HOME/slugify
32
  cd $HOME/slugify
33
+
34
  # Create Go script
35
  cat > main.go << 'EOF'
36
  package main
37
+
38
  import (
39
  "fmt"
40
  "os"
41
  "github.com/gosimple/slug"
42
  )
43
+
44
  func main() {
45
  if len(os.Args) < 2 {
46
  fmt.Println("Usage: slugify <text>")
47
  os.Exit(1)
48
  }
49
+
50
  text := os.Args[1]
51
  slugged := slug.Make(text)
52
  fmt.Println(slugged)
53
  }
54
  EOF
55
+
56
  # Initialize module and install dependency
57
  go mod init slugify
58
  go mod tidy
59
  go get github.com/gosimple/slug
60
+
61
  # Build
62
  go build -o slugify main.go
63
+
64
  # Generate slug
65
  VALUE="${{ inputs.value }}"
66
  echo "Input value: $VALUE"
67
+
68
  SLUG=$(./slugify "$VALUE")
69
  echo "Generated slug: $SLUG"
70
+
71
  # Export
72
+ echo "slug=$SLUG" >> $GITHUB_OUTPUT
README.md CHANGED
@@ -142,6 +142,43 @@ When you select Omni in the UI, Chat UI will:
142
  - Emit RouterMetadata immediately (route and actual model used) so the UI can display it.
143
  - Stream from the selected model via your configured `OPENAI_BASE_URL`. On errors, it tries route fallbacks.
144
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  ## Building
146
 
147
  To create a production version of your app:
 
142
  - Emit RouterMetadata immediately (route and actual model used) so the UI can display it.
143
  - Stream from the selected model via your configured `OPENAI_BASE_URL`. On errors, it tries route fallbacks.
144
 
145
+ Tool and multimodal shortcuts:
146
+
147
+ - Multimodal: If `LLM_ROUTER_ENABLE_MULTIMODAL=true` and the user sends an image, the router bypasses Arch and uses `LLM_ROUTER_MULTIMODAL_MODEL` (or the first multimodal model). Route name: `multimodal`.
148
+ - Tools: If `LLM_ROUTER_ENABLE_TOOLS=true` and the user has at least one MCP server enabled, the router bypasses Arch and uses `LLM_ROUTER_TOOLS_MODEL`. If that model is missing or misconfigured, it falls back to Arch routing. Route name: `agentic`.
149
+
150
+ ### MCP Tools (Optional)
151
+
152
+ Chat UI can call tools exposed by Model Context Protocol (MCP) servers and feed results back to the model using OpenAI function calling. You can preconfigure trusted servers via env, let users add their own, and optionally have the Omni router auto‑select a tools‑capable model.
153
+
154
+ Configure servers (base list for all users):
155
+
156
+ ```env
157
+ # JSON array of servers: name, url, optional headers
158
+ MCP_SERVERS=[
159
+ {"name": "Web Search (Exa)", "url": "https://mcp.exa.ai/mcp"},
160
+ {"name": "Hugging Face MCP Login", "url": "https://huggingface.co/mcp?login"}
161
+ ]
162
+
163
+ # Forward the signed-in user's Hugging Face token to the official HF MCP login endpoint
164
+ # when no Authorization header is set on that server entry.
165
+ MCP_FORWARD_HF_USER_TOKEN=true
166
+ ```
167
+
168
+ Enable router tool path (Omni):
169
+
170
+ - Set `LLM_ROUTER_ENABLE_TOOLS=true` and choose a tools‑capable target with `LLM_ROUTER_TOOLS_MODEL=<model id or name>`.
171
+ - The target must support OpenAI tools/function calling. Chat UI surfaces a “tools” badge on models that advertise this; you can also force‑enable it per‑model in settings (see below).
172
+
173
+ Use tools in the UI:
174
+
175
+ - Open “MCP Servers” from the top‑right menu or from the `+` menu in the chat input to add servers, toggle them on, and run Health Check. The server card lists available tools.
176
+ - When a model calls a tool, the message shows a compact “tool” block with parameters, a progress bar while running, and the result (or error). Results are also provided back to the model for follow‑up.
177
+
178
+ Per‑model overrides:
179
+
180
+ - In Settings → Model, you can toggle “Tool calling (functions)” and “Multimodal input” per model. These overrides apply even if the provider metadata doesn’t advertise the capability.
181
+
182
  ## Building
183
 
184
  To create a production version of your app:
chart/env/dev.yaml CHANGED
@@ -38,8 +38,9 @@ ingressInternal:
38
  envVars:
39
  TEST: "test"
40
  COUPLE_SESSION_WITH_COOKIE_NAME: "token"
41
- OPENID_SCOPES: "openid profile inference-api"
42
  USE_USER_TOKEN: "true"
 
43
  AUTOMATIC_LOGIN: "false"
44
 
45
  ADDRESS_HEADER: "X-Forwarded-For"
@@ -67,6 +68,10 @@ envVars:
67
  LLM_ROUTER_ARCH_TIMEOUT_MS: "10000"
68
  LLM_ROUTER_ENABLE_MULTIMODAL: "true"
69
  LLM_ROUTER_MULTIMODAL_MODEL: "Qwen/Qwen3-VL-235B-A22B-Thinking"
 
 
 
 
70
  PUBLIC_LLM_ROUTER_DISPLAY_NAME: "Omni"
71
  PUBLIC_LLM_ROUTER_LOGO_URL: "https://cdn-uploads.huggingface.co/production/uploads/5f17f0a0925b9863e28ad517/C5V0v1xZXv6M7FXsdJH9b.png"
72
  PUBLIC_LLM_ROUTER_ALIAS_ID: "omni"
 
38
  envVars:
39
  TEST: "test"
40
  COUPLE_SESSION_WITH_COOKIE_NAME: "token"
41
+ OPENID_SCOPES: "openid profile inference-api read-mcp"
42
  USE_USER_TOKEN: "true"
43
+ MCP_FORWARD_HF_USER_TOKEN: "true"
44
  AUTOMATIC_LOGIN: "false"
45
 
46
  ADDRESS_HEADER: "X-Forwarded-For"
 
68
  LLM_ROUTER_ARCH_TIMEOUT_MS: "10000"
69
  LLM_ROUTER_ENABLE_MULTIMODAL: "true"
70
  LLM_ROUTER_MULTIMODAL_MODEL: "Qwen/Qwen3-VL-235B-A22B-Thinking"
71
+ LLM_ROUTER_ENABLE_TOOLS: "true"
72
+ LLM_ROUTER_TOOLS_MODEL: "moonshotai/Kimi-K2-Instruct-0905"
73
+ MCP_SERVERS: >
74
+ [{"name": "Web Search (Exa)", "url": "https://mcp.exa.ai/mcp"}, {"name": "Hugging Face", "url": "https://huggingface.co/mcp?login"}]
75
  PUBLIC_LLM_ROUTER_DISPLAY_NAME: "Omni"
76
  PUBLIC_LLM_ROUTER_LOGO_URL: "https://cdn-uploads.huggingface.co/production/uploads/5f17f0a0925b9863e28ad517/C5V0v1xZXv6M7FXsdJH9b.png"
77
  PUBLIC_LLM_ROUTER_ALIAS_ID: "omni"
chart/env/prod.yaml CHANGED
@@ -48,8 +48,9 @@ ingressInternal:
48
 
49
  envVars:
50
  COUPLE_SESSION_WITH_COOKIE_NAME: "token"
51
- OPENID_SCOPES: "openid profile inference-api"
52
  USE_USER_TOKEN: "true"
 
53
  AUTOMATIC_LOGIN: "false"
54
 
55
  ADDRESS_HEADER: "X-Forwarded-For"
@@ -76,7 +77,11 @@ envVars:
76
  LLM_ROUTER_OTHER_ROUTE: "casual_conversation"
77
  LLM_ROUTER_ARCH_TIMEOUT_MS: "10000"
78
  LLM_ROUTER_ENABLE_MULTIMODAL: "true"
79
- LLM_ROUTER_MULTIMODAL_MODEL: "Qwen/Qwen3-VL-235B-A22B-Thinking"
 
 
 
 
80
  PUBLIC_LLM_ROUTER_DISPLAY_NAME: "Omni"
81
  PUBLIC_LLM_ROUTER_LOGO_URL: "https://cdn-uploads.huggingface.co/production/uploads/5f17f0a0925b9863e28ad517/C5V0v1xZXv6M7FXsdJH9b.png"
82
  PUBLIC_LLM_ROUTER_ALIAS_ID: "omni"
 
48
 
49
  envVars:
50
  COUPLE_SESSION_WITH_COOKIE_NAME: "token"
51
+ OPENID_SCOPES: "openid profile inference-api read-mcp"
52
  USE_USER_TOKEN: "true"
53
+ MCP_FORWARD_HF_USER_TOKEN: "true"
54
  AUTOMATIC_LOGIN: "false"
55
 
56
  ADDRESS_HEADER: "X-Forwarded-For"
 
77
  LLM_ROUTER_OTHER_ROUTE: "casual_conversation"
78
  LLM_ROUTER_ARCH_TIMEOUT_MS: "10000"
79
  LLM_ROUTER_ENABLE_MULTIMODAL: "true"
80
+ LLM_ROUTER_MULTIMODAL_MODEL: "Qwen/Qwen3-VL-30B-A3B-Instruct"
81
+ LLM_ROUTER_ENABLE_TOOLS: "true"
82
+ LLM_ROUTER_TOOLS_MODEL: "moonshotai/Kimi-K2-Instruct-0905"
83
+ MCP_SERVERS: >
84
+ [{"name": "Web Search (Exa)", "url": "https://mcp.exa.ai/mcp"}, {"name": "Hugging Face", "url": "https://huggingface.co/mcp?login"}]
85
  PUBLIC_LLM_ROUTER_DISPLAY_NAME: "Omni"
86
  PUBLIC_LLM_ROUTER_LOGO_URL: "https://cdn-uploads.huggingface.co/production/uploads/5f17f0a0925b9863e28ad517/C5V0v1xZXv6M7FXsdJH9b.png"
87
  PUBLIC_LLM_ROUTER_ALIAS_ID: "omni"
package-lock.json CHANGED
@@ -13,6 +13,7 @@
13
  "@huggingface/hub": "^2.2.0",
14
  "@huggingface/inference": "^4.11.3",
15
  "@iconify-json/bi": "^1.1.21",
 
16
  "@resvg/resvg-js": "^2.6.2",
17
  "autoprefixer": "^10.4.14",
18
  "aws4": "^1.13.2",
@@ -396,24 +397,24 @@
396
  }
397
  },
398
  "node_modules/@aws-sdk/client-cognito-identity": {
399
- "version": "3.925.0",
400
- "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.925.0.tgz",
401
- "integrity": "sha512-7koO8MTU6T0dKAaFi7Bm06t4l8M9z798WSvpwzcCVItf6UAj+popz5MKzomxpd4Ire7C1jqqponiM8rrxNyYcQ==",
402
  "license": "Apache-2.0",
403
  "dependencies": {
404
  "@aws-crypto/sha256-browser": "5.2.0",
405
  "@aws-crypto/sha256-js": "5.2.0",
406
- "@aws-sdk/core": "3.922.0",
407
- "@aws-sdk/credential-provider-node": "3.925.0",
408
  "@aws-sdk/middleware-host-header": "3.922.0",
409
  "@aws-sdk/middleware-logger": "3.922.0",
410
  "@aws-sdk/middleware-recursion-detection": "3.922.0",
411
- "@aws-sdk/middleware-user-agent": "3.922.0",
412
  "@aws-sdk/region-config-resolver": "3.925.0",
413
  "@aws-sdk/types": "3.922.0",
414
  "@aws-sdk/util-endpoints": "3.922.0",
415
  "@aws-sdk/util-user-agent-browser": "3.922.0",
416
- "@aws-sdk/util-user-agent-node": "3.922.0",
417
  "@smithy/config-resolver": "^4.4.2",
418
  "@smithy/core": "^3.17.2",
419
  "@smithy/fetch-http-handler": "^5.3.5",
@@ -446,23 +447,23 @@
446
  }
447
  },
448
  "node_modules/@aws-sdk/client-sso": {
449
- "version": "3.925.0",
450
- "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.925.0.tgz",
451
- "integrity": "sha512-ixC9CyXe/mBo1X+bzOxIIzsdBYzM+klWoHUYzwnPMrXhpDrMjj8D24R/FPqrDnhoYYXiyS4BApRLpeymsFJq2Q==",
452
  "license": "Apache-2.0",
453
  "dependencies": {
454
  "@aws-crypto/sha256-browser": "5.2.0",
455
  "@aws-crypto/sha256-js": "5.2.0",
456
- "@aws-sdk/core": "3.922.0",
457
  "@aws-sdk/middleware-host-header": "3.922.0",
458
  "@aws-sdk/middleware-logger": "3.922.0",
459
  "@aws-sdk/middleware-recursion-detection": "3.922.0",
460
- "@aws-sdk/middleware-user-agent": "3.922.0",
461
  "@aws-sdk/region-config-resolver": "3.925.0",
462
  "@aws-sdk/types": "3.922.0",
463
  "@aws-sdk/util-endpoints": "3.922.0",
464
  "@aws-sdk/util-user-agent-browser": "3.922.0",
465
- "@aws-sdk/util-user-agent-node": "3.922.0",
466
  "@smithy/config-resolver": "^4.4.2",
467
  "@smithy/core": "^3.17.2",
468
  "@smithy/fetch-http-handler": "^5.3.5",
@@ -495,9 +496,9 @@
495
  }
496
  },
497
  "node_modules/@aws-sdk/core": {
498
- "version": "3.922.0",
499
- "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.922.0.tgz",
500
- "integrity": "sha512-EvfP4cqJfpO3L2v5vkIlTkMesPtRwWlMfsaW6Tpfm7iYfBOuTi6jx60pMDMTyJNVfh6cGmXwh/kj1jQdR+w99Q==",
501
  "license": "Apache-2.0",
502
  "dependencies": {
503
  "@aws-sdk/types": "3.922.0",
@@ -519,12 +520,12 @@
519
  }
520
  },
521
  "node_modules/@aws-sdk/credential-provider-cognito-identity": {
522
- "version": "3.925.0",
523
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.925.0.tgz",
524
- "integrity": "sha512-hSA6PE/u+DYYJVJ01cyKiDR3d31kOJ1l+qJJimEiG+jH1K+EUgjhNVZKHUzEbumVvpWVHeZJ7Hs6iq4F/rS4+g==",
525
  "license": "Apache-2.0",
526
  "dependencies": {
527
- "@aws-sdk/client-cognito-identity": "3.925.0",
528
  "@aws-sdk/types": "3.922.0",
529
  "@smithy/property-provider": "^4.2.4",
530
  "@smithy/types": "^4.8.1",
@@ -535,12 +536,12 @@
535
  }
536
  },
537
  "node_modules/@aws-sdk/credential-provider-env": {
538
- "version": "3.922.0",
539
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.922.0.tgz",
540
- "integrity": "sha512-WikGQpKkROJSK3D3E7odPjZ8tU7WJp5/TgGdRuZw3izsHUeH48xMv6IznafpRTmvHcjAbDQj4U3CJZNAzOK/OQ==",
541
  "license": "Apache-2.0",
542
  "dependencies": {
543
- "@aws-sdk/core": "3.922.0",
544
  "@aws-sdk/types": "3.922.0",
545
  "@smithy/property-provider": "^4.2.4",
546
  "@smithy/types": "^4.8.1",
@@ -551,12 +552,12 @@
551
  }
552
  },
553
  "node_modules/@aws-sdk/credential-provider-http": {
554
- "version": "3.922.0",
555
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.922.0.tgz",
556
- "integrity": "sha512-i72DgHMK7ydAEqdzU0Duqh60Q8W59EZmRJ73y0Y5oFmNOqnYsAI+UXyOoCsubp+Dkr6+yOwAn1gPt1XGE9Aowg==",
557
  "license": "Apache-2.0",
558
  "dependencies": {
559
- "@aws-sdk/core": "3.922.0",
560
  "@aws-sdk/types": "3.922.0",
561
  "@smithy/fetch-http-handler": "^5.3.5",
562
  "@smithy/node-http-handler": "^4.4.4",
@@ -572,18 +573,18 @@
572
  }
573
  },
574
  "node_modules/@aws-sdk/credential-provider-ini": {
575
- "version": "3.925.0",
576
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.925.0.tgz",
577
- "integrity": "sha512-TOs/UkKWwXrSPolRTChpDUQjczw6KqbbanF0EzjUm3sp/AS1ThOQCKuTTdaOBZXkCIJdvRmZjF3adccE3rAoXg==",
578
  "license": "Apache-2.0",
579
  "dependencies": {
580
- "@aws-sdk/core": "3.922.0",
581
- "@aws-sdk/credential-provider-env": "3.922.0",
582
- "@aws-sdk/credential-provider-http": "3.922.0",
583
- "@aws-sdk/credential-provider-process": "3.922.0",
584
- "@aws-sdk/credential-provider-sso": "3.925.0",
585
- "@aws-sdk/credential-provider-web-identity": "3.925.0",
586
- "@aws-sdk/nested-clients": "3.925.0",
587
  "@aws-sdk/types": "3.922.0",
588
  "@smithy/credential-provider-imds": "^4.2.4",
589
  "@smithy/property-provider": "^4.2.4",
@@ -596,17 +597,17 @@
596
  }
597
  },
598
  "node_modules/@aws-sdk/credential-provider-node": {
599
- "version": "3.925.0",
600
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.925.0.tgz",
601
- "integrity": "sha512-+T9mnnTY73MLkVxsk5RtzE4fv7GnMhR7iXhL/yTusf1zLfA09uxlA9VCz6tWxm5rHcO4ZN0x4hnqqDhM+DB5KQ==",
602
  "license": "Apache-2.0",
603
  "dependencies": {
604
- "@aws-sdk/credential-provider-env": "3.922.0",
605
- "@aws-sdk/credential-provider-http": "3.922.0",
606
- "@aws-sdk/credential-provider-ini": "3.925.0",
607
- "@aws-sdk/credential-provider-process": "3.922.0",
608
- "@aws-sdk/credential-provider-sso": "3.925.0",
609
- "@aws-sdk/credential-provider-web-identity": "3.925.0",
610
  "@aws-sdk/types": "3.922.0",
611
  "@smithy/credential-provider-imds": "^4.2.4",
612
  "@smithy/property-provider": "^4.2.4",
@@ -619,12 +620,12 @@
619
  }
620
  },
621
  "node_modules/@aws-sdk/credential-provider-process": {
622
- "version": "3.922.0",
623
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.922.0.tgz",
624
- "integrity": "sha512-1DZOYezT6okslpvMW7oA2q+y17CJd4fxjNFH0jtThfswdh9CtG62+wxenqO+NExttq0UMaKisrkZiVrYQBTShw==",
625
  "license": "Apache-2.0",
626
  "dependencies": {
627
- "@aws-sdk/core": "3.922.0",
628
  "@aws-sdk/types": "3.922.0",
629
  "@smithy/property-provider": "^4.2.4",
630
  "@smithy/shared-ini-file-loader": "^4.3.4",
@@ -636,14 +637,14 @@
636
  }
637
  },
638
  "node_modules/@aws-sdk/credential-provider-sso": {
639
- "version": "3.925.0",
640
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.925.0.tgz",
641
- "integrity": "sha512-aZlUC6LRsOMDvIu0ifF62mTjL3KGzclWu5XBBN8eLDAYTdhqMxv3HyrqWoiHnGZnZGaVU+II+qsVoeBnGOwHow==",
642
  "license": "Apache-2.0",
643
  "dependencies": {
644
- "@aws-sdk/client-sso": "3.925.0",
645
- "@aws-sdk/core": "3.922.0",
646
- "@aws-sdk/token-providers": "3.925.0",
647
  "@aws-sdk/types": "3.922.0",
648
  "@smithy/property-provider": "^4.2.4",
649
  "@smithy/shared-ini-file-loader": "^4.3.4",
@@ -655,13 +656,13 @@
655
  }
656
  },
657
  "node_modules/@aws-sdk/credential-provider-web-identity": {
658
- "version": "3.925.0",
659
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.925.0.tgz",
660
- "integrity": "sha512-dR34s8Sfd1wJBzIuvRFO2FCnLmYD8iwPWrdXWI2ZypFt1EQR8jeQ20mnS+UOCoR5Z0tY6wJqEgTXKl4KuZ+DUg==",
661
  "license": "Apache-2.0",
662
  "dependencies": {
663
- "@aws-sdk/core": "3.922.0",
664
- "@aws-sdk/nested-clients": "3.925.0",
665
  "@aws-sdk/types": "3.922.0",
666
  "@smithy/property-provider": "^4.2.4",
667
  "@smithy/shared-ini-file-loader": "^4.3.4",
@@ -673,22 +674,22 @@
673
  }
674
  },
675
  "node_modules/@aws-sdk/credential-providers": {
676
- "version": "3.925.0",
677
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.925.0.tgz",
678
- "integrity": "sha512-CTTFn+8NiXRoyKbaTKXCSZ9pUs3R3HllTgl2In8Mxl60Eim9QrP3QYbSjH+pqaIOf1qhbe1UuEICzGrO3Y+8MA==",
679
  "license": "Apache-2.0",
680
  "dependencies": {
681
- "@aws-sdk/client-cognito-identity": "3.925.0",
682
- "@aws-sdk/core": "3.922.0",
683
- "@aws-sdk/credential-provider-cognito-identity": "3.925.0",
684
- "@aws-sdk/credential-provider-env": "3.922.0",
685
- "@aws-sdk/credential-provider-http": "3.922.0",
686
- "@aws-sdk/credential-provider-ini": "3.925.0",
687
- "@aws-sdk/credential-provider-node": "3.925.0",
688
- "@aws-sdk/credential-provider-process": "3.922.0",
689
- "@aws-sdk/credential-provider-sso": "3.925.0",
690
- "@aws-sdk/credential-provider-web-identity": "3.925.0",
691
- "@aws-sdk/nested-clients": "3.925.0",
692
  "@aws-sdk/types": "3.922.0",
693
  "@smithy/config-resolver": "^4.4.2",
694
  "@smithy/core": "^3.17.2",
@@ -748,12 +749,12 @@
748
  }
749
  },
750
  "node_modules/@aws-sdk/middleware-user-agent": {
751
- "version": "3.922.0",
752
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.922.0.tgz",
753
- "integrity": "sha512-N4Qx/9KP3oVQBJOrSghhz8iZFtUC2NNeSZt88hpPhbqAEAtuX8aD8OzVcpnAtrwWqy82Yd2YTxlkqMGkgqnBsQ==",
754
  "license": "Apache-2.0",
755
  "dependencies": {
756
- "@aws-sdk/core": "3.922.0",
757
  "@aws-sdk/types": "3.922.0",
758
  "@aws-sdk/util-endpoints": "3.922.0",
759
  "@smithy/core": "^3.17.2",
@@ -766,23 +767,23 @@
766
  }
767
  },
768
  "node_modules/@aws-sdk/nested-clients": {
769
- "version": "3.925.0",
770
- "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.925.0.tgz",
771
- "integrity": "sha512-Fc8QhH+1YzGQb5aWQUX6gRnKSzUZ9p3p/muqXIgYBL8RSd5O6hSPhDTyrOWE247zFlOjVlAlEnoTMJKarH0cIA==",
772
  "license": "Apache-2.0",
773
  "dependencies": {
774
  "@aws-crypto/sha256-browser": "5.2.0",
775
  "@aws-crypto/sha256-js": "5.2.0",
776
- "@aws-sdk/core": "3.922.0",
777
  "@aws-sdk/middleware-host-header": "3.922.0",
778
  "@aws-sdk/middleware-logger": "3.922.0",
779
  "@aws-sdk/middleware-recursion-detection": "3.922.0",
780
- "@aws-sdk/middleware-user-agent": "3.922.0",
781
  "@aws-sdk/region-config-resolver": "3.925.0",
782
  "@aws-sdk/types": "3.922.0",
783
  "@aws-sdk/util-endpoints": "3.922.0",
784
  "@aws-sdk/util-user-agent-browser": "3.922.0",
785
- "@aws-sdk/util-user-agent-node": "3.922.0",
786
  "@smithy/config-resolver": "^4.4.2",
787
  "@smithy/core": "^3.17.2",
788
  "@smithy/fetch-http-handler": "^5.3.5",
@@ -831,13 +832,13 @@
831
  }
832
  },
833
  "node_modules/@aws-sdk/token-providers": {
834
- "version": "3.925.0",
835
- "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.925.0.tgz",
836
- "integrity": "sha512-F4Oibka1W5YYDeL+rGt/Hg3NLjOzrJdmuZOE0OFQt/U6dnJwYmYi2gFqduvZnZcD1agNm37mh7/GUq1zvKS6ig==",
837
  "license": "Apache-2.0",
838
  "dependencies": {
839
- "@aws-sdk/core": "3.922.0",
840
- "@aws-sdk/nested-clients": "3.925.0",
841
  "@aws-sdk/types": "3.922.0",
842
  "@smithy/property-provider": "^4.2.4",
843
  "@smithy/shared-ini-file-loader": "^4.3.4",
@@ -902,12 +903,12 @@
902
  }
903
  },
904
  "node_modules/@aws-sdk/util-user-agent-node": {
905
- "version": "3.922.0",
906
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.922.0.tgz",
907
- "integrity": "sha512-NrPe/Rsr5kcGunkog0eBV+bY0inkRELsD2SacC4lQZvZiXf8VJ2Y7j+Yq1tB+h+FPLsdt3v9wItIvDf/laAm0Q==",
908
  "license": "Apache-2.0",
909
  "dependencies": {
910
- "@aws-sdk/middleware-user-agent": "3.922.0",
911
  "@aws-sdk/types": "3.922.0",
912
  "@smithy/node-config-provider": "^4.3.4",
913
  "@smithy/types": "^4.8.1",
@@ -2421,6 +2422,60 @@
2421
  "@jridgewell/sourcemap-codec": "^1.4.14"
2422
  }
2423
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2424
  "node_modules/@mongodb-js/saslprep": {
2425
  "version": "1.2.2",
2426
  "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.2.tgz",
@@ -4528,6 +4583,40 @@
4528
  "node": ">=6.5"
4529
  }
4530
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4531
  "node_modules/acorn": {
4532
  "version": "8.14.1",
4533
  "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
@@ -4589,6 +4678,45 @@
4589
  "url": "https://github.com/sponsors/epoberezkin"
4590
  }
4591
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4592
  "node_modules/ansi-escapes": {
4593
  "version": "7.0.0",
4594
  "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz",
@@ -4868,6 +4996,26 @@
4868
  "svelte": "^5.33.0"
4869
  }
4870
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4871
  "node_modules/bowser": {
4872
  "version": "2.12.1",
4873
  "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz",
@@ -4986,6 +5134,15 @@
4986
  "node": "*"
4987
  }
4988
  },
 
 
 
 
 
 
 
 
 
4989
  "node_modules/cac": {
4990
  "version": "6.7.14",
4991
  "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
@@ -5008,6 +5165,22 @@
5008
  "node": ">= 0.4"
5009
  }
5010
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5011
  "node_modules/callsites": {
5012
  "version": "3.1.0",
5013
  "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -5255,6 +5428,27 @@
5255
  "dev": true,
5256
  "license": "MIT"
5257
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5258
  "node_modules/cookie": {
5259
  "version": "0.6.0",
5260
  "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
@@ -5265,6 +5459,15 @@
5265
  "node": ">= 0.6"
5266
  }
5267
  },
 
 
 
 
 
 
 
 
 
5268
  "node_modules/copy-anything": {
5269
  "version": "3.0.5",
5270
  "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
@@ -5281,6 +5484,19 @@
5281
  "url": "https://github.com/sponsors/mesqueeb"
5282
  }
5283
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
5284
  "node_modules/cross-spawn": {
5285
  "version": "7.0.6",
5286
  "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -5448,6 +5664,15 @@
5448
  "node": ">=0.4.0"
5449
  }
5450
  },
 
 
 
 
 
 
 
 
 
5451
  "node_modules/dequal": {
5452
  "version": "2.0.3",
5453
  "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -5573,6 +5798,12 @@
5573
  "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
5574
  "license": "MIT"
5575
  },
 
 
 
 
 
 
5576
  "node_modules/electron-to-chromium": {
5577
  "version": "1.5.165",
5578
  "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.165.tgz",
@@ -5616,6 +5847,15 @@
5616
  "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
5617
  "license": "MIT"
5618
  },
 
 
 
 
 
 
 
 
 
5619
  "node_modules/end-of-stream": {
5620
  "version": "1.4.4",
5621
  "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@@ -6043,6 +6283,15 @@
6043
  "node": ">=0.10.0"
6044
  }
6045
  },
 
 
 
 
 
 
 
 
 
6046
  "node_modules/event-target-shim": {
6047
  "version": "5.0.1",
6048
  "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
@@ -6067,6 +6316,27 @@
6067
  "node": ">=0.8.x"
6068
  }
6069
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6070
  "node_modules/exact-mirror": {
6071
  "version": "0.1.2",
6072
  "resolved": "https://registry.npmjs.org/exact-mirror/-/exact-mirror-0.1.2.tgz",
@@ -6125,6 +6395,93 @@
6125
  "node": ">=12.0.0"
6126
  }
6127
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6128
  "node_modules/exsolve": {
6129
  "version": "1.0.5",
6130
  "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.5.tgz",
@@ -6148,7 +6505,6 @@
6148
  "version": "3.1.3",
6149
  "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
6150
  "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
6151
- "dev": true,
6152
  "license": "MIT"
6153
  },
6154
  "node_modules/fast-fifo": {
@@ -6215,6 +6571,22 @@
6215
  "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
6216
  "license": "MIT"
6217
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6218
  "node_modules/fast-xml-parser": {
6219
  "version": "5.2.5",
6220
  "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz",
@@ -6305,6 +6677,23 @@
6305
  "node": ">=8"
6306
  }
6307
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6308
  "node_modules/find-cache-dir": {
6309
  "version": "3.3.2",
6310
  "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
@@ -6434,6 +6823,15 @@
6434
  "node": ">= 12.20"
6435
  }
6436
  },
 
 
 
 
 
 
 
 
 
6437
  "node_modules/fraction.js": {
6438
  "version": "4.3.7",
6439
  "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -6447,6 +6845,15 @@
6447
  "url": "https://github.com/sponsors/rawify"
6448
  }
6449
  },
 
 
 
 
 
 
 
 
 
6450
  "node_modules/fs.realpath": {
6451
  "version": "1.0.0",
6452
  "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -6766,6 +7173,31 @@
6766
  "node": ">=12"
6767
  }
6768
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6769
  "node_modules/http-proxy-agent": {
6770
  "version": "5.0.0",
6771
  "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
@@ -6924,7 +7356,6 @@
6924
  "version": "2.0.4",
6925
  "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
6926
  "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
6927
- "dev": true,
6928
  "license": "ISC"
6929
  },
6930
  "node_modules/inline-style-parser": {
@@ -6952,6 +7383,15 @@
6952
  "node": ">= 12"
6953
  }
6954
  },
 
 
 
 
 
 
 
 
 
6955
  "node_modules/is-arrayish": {
6956
  "version": "0.3.2",
6957
  "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
@@ -7050,6 +7490,12 @@
7050
  "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
7051
  "license": "MIT"
7052
  },
 
 
 
 
 
 
7053
  "node_modules/is-reference": {
7054
  "version": "1.2.1",
7055
  "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
@@ -7854,6 +8300,15 @@
7854
  "node": ">= 0.4"
7855
  }
7856
  },
 
 
 
 
 
 
 
 
 
7857
  "node_modules/memory-pager": {
7858
  "version": "1.5.0",
7859
  "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
@@ -7861,6 +8316,18 @@
7861
  "devOptional": true,
7862
  "license": "MIT"
7863
  },
 
 
 
 
 
 
 
 
 
 
 
 
7864
  "node_modules/merge-stream": {
7865
  "version": "2.0.0",
7866
  "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -8281,6 +8748,15 @@
8281
  "dev": true,
8282
  "license": "MIT"
8283
  },
 
 
 
 
 
 
 
 
 
8284
  "node_modules/neo-async": {
8285
  "version": "2.6.2",
8286
  "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
@@ -8443,6 +8919,18 @@
8443
  "node": ">= 6"
8444
  }
8445
  },
 
 
 
 
 
 
 
 
 
 
 
 
8446
  "node_modules/object-stream": {
8447
  "version": "0.0.1",
8448
  "resolved": "https://registry.npmjs.org/object-stream/-/object-stream-0.0.1.tgz",
@@ -8469,6 +8957,18 @@
8469
  "node": ">=14.0.0"
8470
  }
8471
  },
 
 
 
 
 
 
 
 
 
 
 
 
8472
  "node_modules/once": {
8473
  "version": "1.4.0",
8474
  "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -8712,6 +9212,15 @@
8712
  "url": "https://github.com/inikulin/parse5?sponsor=1"
8713
  }
8714
  },
 
 
 
 
 
 
 
 
 
8715
  "node_modules/path-exists": {
8716
  "version": "4.0.0",
8717
  "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -8769,6 +9278,16 @@
8769
  "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
8770
  "license": "ISC"
8771
  },
 
 
 
 
 
 
 
 
 
 
8772
  "node_modules/path-type": {
8773
  "version": "4.0.0",
8774
  "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -8924,6 +9443,15 @@
8924
  "node": ">= 6"
8925
  }
8926
  },
 
 
 
 
 
 
 
 
 
8927
  "node_modules/pkg-dir": {
8928
  "version": "4.2.0",
8929
  "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
@@ -9460,6 +9988,19 @@
9460
  "node": "^16 || ^18 || >=20"
9461
  }
9462
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
9463
  "node_modules/psl": {
9464
  "version": "1.15.0",
9465
  "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
@@ -9502,6 +10043,21 @@
9502
  "teleport": ">=0.2.0"
9503
  }
9504
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9505
  "node_modules/quansync": {
9506
  "version": "0.2.10",
9507
  "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz",
@@ -9551,6 +10107,46 @@
9551
  "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
9552
  "license": "MIT"
9553
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9554
  "node_modules/react-is": {
9555
  "version": "17.0.2",
9556
  "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@@ -9606,6 +10202,15 @@
9606
  "node": ">= 12.13.0"
9607
  }
9608
  },
 
 
 
 
 
 
 
 
 
9609
  "node_modules/requires-port": {
9610
  "version": "1.0.0",
9611
  "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -9751,6 +10356,22 @@
9751
  "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
9752
  "license": "MIT"
9753
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9754
  "node_modules/rrweb-cssom": {
9755
  "version": "0.6.0",
9756
  "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz",
@@ -9912,6 +10533,64 @@
9912
  "node": ">=10"
9913
  }
9914
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9915
  "node_modules/set-cookie-parser": {
9916
  "version": "2.7.1",
9917
  "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
@@ -9919,6 +10598,12 @@
9919
  "devOptional": true,
9920
  "license": "MIT"
9921
  },
 
 
 
 
 
 
9922
  "node_modules/sharp": {
9923
  "version": "0.33.5",
9924
  "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
@@ -9979,6 +10664,78 @@
9979
  "node": ">=8"
9980
  }
9981
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9982
  "node_modules/siginfo": {
9983
  "version": "2.0.0",
9984
  "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
@@ -10146,6 +10903,15 @@
10146
  "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
10147
  "license": "MIT"
10148
  },
 
 
 
 
 
 
 
 
 
10149
  "node_modules/std-env": {
10150
  "version": "3.9.0",
10151
  "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
@@ -10911,6 +11677,15 @@
10911
  "node": ">=8.0"
10912
  }
10913
  },
 
 
 
 
 
 
 
 
 
10914
  "node_modules/token-types": {
10915
  "version": "6.0.0",
10916
  "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz",
@@ -11015,6 +11790,41 @@
11015
  "url": "https://github.com/sponsors/sindresorhus"
11016
  }
11017
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11018
  "node_modules/typescript": {
11019
  "version": "5.8.3",
11020
  "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
@@ -11100,6 +11910,15 @@
11100
  "node": ">= 4.0.0"
11101
  }
11102
  },
 
 
 
 
 
 
 
 
 
11103
  "node_modules/unplugin": {
11104
  "version": "1.16.1",
11105
  "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz",
@@ -11232,6 +12051,15 @@
11232
  "integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow==",
11233
  "license": "MIT"
11234
  },
 
 
 
 
 
 
 
 
 
11235
  "node_modules/vite": {
11236
  "version": "6.3.5",
11237
  "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
@@ -11819,6 +12647,15 @@
11819
  "funding": {
11820
  "url": "https://github.com/sponsors/colinhacks"
11821
  }
 
 
 
 
 
 
 
 
 
11822
  }
11823
  }
11824
  }
 
13
  "@huggingface/hub": "^2.2.0",
14
  "@huggingface/inference": "^4.11.3",
15
  "@iconify-json/bi": "^1.1.21",
16
+ "@modelcontextprotocol/sdk": "^1.21.1",
17
  "@resvg/resvg-js": "^2.6.2",
18
  "autoprefixer": "^10.4.14",
19
  "aws4": "^1.13.2",
 
397
  }
398
  },
399
  "node_modules/@aws-sdk/client-cognito-identity": {
400
+ "version": "3.927.0",
401
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.927.0.tgz",
402
+ "integrity": "sha512-nt6qcS94C88jV3ZVzc7nG4ew4Wrbi27UsYFB8OpvLNFSXOTWx3Sd7g7xn6FyRFBM6QH+zijqgQ6lpKIMQdm9+w==",
403
  "license": "Apache-2.0",
404
  "dependencies": {
405
  "@aws-crypto/sha256-browser": "5.2.0",
406
  "@aws-crypto/sha256-js": "5.2.0",
407
+ "@aws-sdk/core": "3.927.0",
408
+ "@aws-sdk/credential-provider-node": "3.927.0",
409
  "@aws-sdk/middleware-host-header": "3.922.0",
410
  "@aws-sdk/middleware-logger": "3.922.0",
411
  "@aws-sdk/middleware-recursion-detection": "3.922.0",
412
+ "@aws-sdk/middleware-user-agent": "3.927.0",
413
  "@aws-sdk/region-config-resolver": "3.925.0",
414
  "@aws-sdk/types": "3.922.0",
415
  "@aws-sdk/util-endpoints": "3.922.0",
416
  "@aws-sdk/util-user-agent-browser": "3.922.0",
417
+ "@aws-sdk/util-user-agent-node": "3.927.0",
418
  "@smithy/config-resolver": "^4.4.2",
419
  "@smithy/core": "^3.17.2",
420
  "@smithy/fetch-http-handler": "^5.3.5",
 
447
  }
448
  },
449
  "node_modules/@aws-sdk/client-sso": {
450
+ "version": "3.927.0",
451
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.927.0.tgz",
452
+ "integrity": "sha512-O+e+jo6ei7U/BA7lhT4mmPCWmeR9dFgGUHVwCwJ5c/nCaSaHQ+cb7j2h8WPXERu0LhPSFyj1aD5dk3jFIwNlbg==",
453
  "license": "Apache-2.0",
454
  "dependencies": {
455
  "@aws-crypto/sha256-browser": "5.2.0",
456
  "@aws-crypto/sha256-js": "5.2.0",
457
+ "@aws-sdk/core": "3.927.0",
458
  "@aws-sdk/middleware-host-header": "3.922.0",
459
  "@aws-sdk/middleware-logger": "3.922.0",
460
  "@aws-sdk/middleware-recursion-detection": "3.922.0",
461
+ "@aws-sdk/middleware-user-agent": "3.927.0",
462
  "@aws-sdk/region-config-resolver": "3.925.0",
463
  "@aws-sdk/types": "3.922.0",
464
  "@aws-sdk/util-endpoints": "3.922.0",
465
  "@aws-sdk/util-user-agent-browser": "3.922.0",
466
+ "@aws-sdk/util-user-agent-node": "3.927.0",
467
  "@smithy/config-resolver": "^4.4.2",
468
  "@smithy/core": "^3.17.2",
469
  "@smithy/fetch-http-handler": "^5.3.5",
 
496
  }
497
  },
498
  "node_modules/@aws-sdk/core": {
499
+ "version": "3.927.0",
500
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.927.0.tgz",
501
+ "integrity": "sha512-QOtR9QdjNeC7bId3fc/6MnqoEezvQ2Fk+x6F+Auf7NhOxwYAtB1nvh0k3+gJHWVGpfxN1I8keahRZd79U68/ag==",
502
  "license": "Apache-2.0",
503
  "dependencies": {
504
  "@aws-sdk/types": "3.922.0",
 
520
  }
521
  },
522
  "node_modules/@aws-sdk/credential-provider-cognito-identity": {
523
+ "version": "3.927.0",
524
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.927.0.tgz",
525
+ "integrity": "sha512-zV6w71IT+7rTUiIBIdzHt0aDkYA0NckZHr97/O6qcp0qm3mIj8oiDjHo6sD8qLAVT2ixmAhuBuZ8DAkMHjZ0wA==",
526
  "license": "Apache-2.0",
527
  "dependencies": {
528
+ "@aws-sdk/client-cognito-identity": "3.927.0",
529
  "@aws-sdk/types": "3.922.0",
530
  "@smithy/property-provider": "^4.2.4",
531
  "@smithy/types": "^4.8.1",
 
536
  }
537
  },
538
  "node_modules/@aws-sdk/credential-provider-env": {
539
+ "version": "3.927.0",
540
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.927.0.tgz",
541
+ "integrity": "sha512-bAllBpmaWINpf0brXQWh/hjkBctapknZPYb3FJRlBHytEGHi7TpgqBXi8riT0tc6RVWChhnw58rQz22acOmBuw==",
542
  "license": "Apache-2.0",
543
  "dependencies": {
544
+ "@aws-sdk/core": "3.927.0",
545
  "@aws-sdk/types": "3.922.0",
546
  "@smithy/property-provider": "^4.2.4",
547
  "@smithy/types": "^4.8.1",
 
552
  }
553
  },
554
  "node_modules/@aws-sdk/credential-provider-http": {
555
+ "version": "3.927.0",
556
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.927.0.tgz",
557
+ "integrity": "sha512-jEvb8C7tuRBFhe8vZY9vm9z6UQnbP85IMEt3Qiz0dxAd341Hgu0lOzMv5mSKQ5yBnTLq+t3FPKgD9tIiHLqxSQ==",
558
  "license": "Apache-2.0",
559
  "dependencies": {
560
+ "@aws-sdk/core": "3.927.0",
561
  "@aws-sdk/types": "3.922.0",
562
  "@smithy/fetch-http-handler": "^5.3.5",
563
  "@smithy/node-http-handler": "^4.4.4",
 
573
  }
574
  },
575
  "node_modules/@aws-sdk/credential-provider-ini": {
576
+ "version": "3.927.0",
577
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.927.0.tgz",
578
+ "integrity": "sha512-WvliaKYT7bNLiryl/FsZyUwRGBo/CWtboekZWvSfloAb+0SKFXWjmxt3z+Y260aoaPm/LIzEyslDHfxqR9xCJQ==",
579
  "license": "Apache-2.0",
580
  "dependencies": {
581
+ "@aws-sdk/core": "3.927.0",
582
+ "@aws-sdk/credential-provider-env": "3.927.0",
583
+ "@aws-sdk/credential-provider-http": "3.927.0",
584
+ "@aws-sdk/credential-provider-process": "3.927.0",
585
+ "@aws-sdk/credential-provider-sso": "3.927.0",
586
+ "@aws-sdk/credential-provider-web-identity": "3.927.0",
587
+ "@aws-sdk/nested-clients": "3.927.0",
588
  "@aws-sdk/types": "3.922.0",
589
  "@smithy/credential-provider-imds": "^4.2.4",
590
  "@smithy/property-provider": "^4.2.4",
 
597
  }
598
  },
599
  "node_modules/@aws-sdk/credential-provider-node": {
600
+ "version": "3.927.0",
601
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.927.0.tgz",
602
+ "integrity": "sha512-M6BLrI+WHQ7PUY1aYu2OkI/KEz9aca+05zyycACk7cnlHlZaQ3vTFd0xOqF+A1qaenQBuxApOTs7Z21pnPUo9Q==",
603
  "license": "Apache-2.0",
604
  "dependencies": {
605
+ "@aws-sdk/credential-provider-env": "3.927.0",
606
+ "@aws-sdk/credential-provider-http": "3.927.0",
607
+ "@aws-sdk/credential-provider-ini": "3.927.0",
608
+ "@aws-sdk/credential-provider-process": "3.927.0",
609
+ "@aws-sdk/credential-provider-sso": "3.927.0",
610
+ "@aws-sdk/credential-provider-web-identity": "3.927.0",
611
  "@aws-sdk/types": "3.922.0",
612
  "@smithy/credential-provider-imds": "^4.2.4",
613
  "@smithy/property-provider": "^4.2.4",
 
620
  }
621
  },
622
  "node_modules/@aws-sdk/credential-provider-process": {
623
+ "version": "3.927.0",
624
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.927.0.tgz",
625
+ "integrity": "sha512-rvqdZIN3TRhLKssufN5G2EWLMBct3ZebOBdwr0tuOoPEdaYflyXYYUScu+Beb541CKfXaFnEOlZokq12r7EPcQ==",
626
  "license": "Apache-2.0",
627
  "dependencies": {
628
+ "@aws-sdk/core": "3.927.0",
629
  "@aws-sdk/types": "3.922.0",
630
  "@smithy/property-provider": "^4.2.4",
631
  "@smithy/shared-ini-file-loader": "^4.3.4",
 
637
  }
638
  },
639
  "node_modules/@aws-sdk/credential-provider-sso": {
640
+ "version": "3.927.0",
641
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.927.0.tgz",
642
+ "integrity": "sha512-XrCuncze/kxZE6WYEWtNMGtrJvJtyhUqav4xQQ9PJcNjxCUYiIRv7Gwkt7cuwJ1HS+akQj+JiZmljAg97utfDw==",
643
  "license": "Apache-2.0",
644
  "dependencies": {
645
+ "@aws-sdk/client-sso": "3.927.0",
646
+ "@aws-sdk/core": "3.927.0",
647
+ "@aws-sdk/token-providers": "3.927.0",
648
  "@aws-sdk/types": "3.922.0",
649
  "@smithy/property-provider": "^4.2.4",
650
  "@smithy/shared-ini-file-loader": "^4.3.4",
 
656
  }
657
  },
658
  "node_modules/@aws-sdk/credential-provider-web-identity": {
659
+ "version": "3.927.0",
660
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.927.0.tgz",
661
+ "integrity": "sha512-Oh/aFYjZQsIiZ2PQEgTNvqEE/mmOYxZKZzXV86qrU3jBUfUUBvprUZc684nBqJbSKPwM5jCZtxiRYh+IrZDE7A==",
662
  "license": "Apache-2.0",
663
  "dependencies": {
664
+ "@aws-sdk/core": "3.927.0",
665
+ "@aws-sdk/nested-clients": "3.927.0",
666
  "@aws-sdk/types": "3.922.0",
667
  "@smithy/property-provider": "^4.2.4",
668
  "@smithy/shared-ini-file-loader": "^4.3.4",
 
674
  }
675
  },
676
  "node_modules/@aws-sdk/credential-providers": {
677
+ "version": "3.927.0",
678
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.927.0.tgz",
679
+ "integrity": "sha512-CasoHKKE/K+6YcVqjE+v5dVyKqKBtfzZyvGi669HvJ1f4EPHbVRPPLIb0eAYd/aEmwHsB/nn9VnyN9Wq5OppUQ==",
680
  "license": "Apache-2.0",
681
  "dependencies": {
682
+ "@aws-sdk/client-cognito-identity": "3.927.0",
683
+ "@aws-sdk/core": "3.927.0",
684
+ "@aws-sdk/credential-provider-cognito-identity": "3.927.0",
685
+ "@aws-sdk/credential-provider-env": "3.927.0",
686
+ "@aws-sdk/credential-provider-http": "3.927.0",
687
+ "@aws-sdk/credential-provider-ini": "3.927.0",
688
+ "@aws-sdk/credential-provider-node": "3.927.0",
689
+ "@aws-sdk/credential-provider-process": "3.927.0",
690
+ "@aws-sdk/credential-provider-sso": "3.927.0",
691
+ "@aws-sdk/credential-provider-web-identity": "3.927.0",
692
+ "@aws-sdk/nested-clients": "3.927.0",
693
  "@aws-sdk/types": "3.922.0",
694
  "@smithy/config-resolver": "^4.4.2",
695
  "@smithy/core": "^3.17.2",
 
749
  }
750
  },
751
  "node_modules/@aws-sdk/middleware-user-agent": {
752
+ "version": "3.927.0",
753
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.927.0.tgz",
754
+ "integrity": "sha512-sv6St9EgEka6E7y19UMCsttFBZ8tsmz2sstgRd7LztlX3wJynpeDUhq0gtedguG1lGZY/gDf832k5dqlRLUk7g==",
755
  "license": "Apache-2.0",
756
  "dependencies": {
757
+ "@aws-sdk/core": "3.927.0",
758
  "@aws-sdk/types": "3.922.0",
759
  "@aws-sdk/util-endpoints": "3.922.0",
760
  "@smithy/core": "^3.17.2",
 
767
  }
768
  },
769
  "node_modules/@aws-sdk/nested-clients": {
770
+ "version": "3.927.0",
771
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.927.0.tgz",
772
+ "integrity": "sha512-Oy6w7+fzIdr10DhF/HpfVLy6raZFTdiE7pxS1rvpuj2JgxzW2y6urm2sYf3eLOpMiHyuG4xUBwFiJpU9CCEvJA==",
773
  "license": "Apache-2.0",
774
  "dependencies": {
775
  "@aws-crypto/sha256-browser": "5.2.0",
776
  "@aws-crypto/sha256-js": "5.2.0",
777
+ "@aws-sdk/core": "3.927.0",
778
  "@aws-sdk/middleware-host-header": "3.922.0",
779
  "@aws-sdk/middleware-logger": "3.922.0",
780
  "@aws-sdk/middleware-recursion-detection": "3.922.0",
781
+ "@aws-sdk/middleware-user-agent": "3.927.0",
782
  "@aws-sdk/region-config-resolver": "3.925.0",
783
  "@aws-sdk/types": "3.922.0",
784
  "@aws-sdk/util-endpoints": "3.922.0",
785
  "@aws-sdk/util-user-agent-browser": "3.922.0",
786
+ "@aws-sdk/util-user-agent-node": "3.927.0",
787
  "@smithy/config-resolver": "^4.4.2",
788
  "@smithy/core": "^3.17.2",
789
  "@smithy/fetch-http-handler": "^5.3.5",
 
832
  }
833
  },
834
  "node_modules/@aws-sdk/token-providers": {
835
+ "version": "3.927.0",
836
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.927.0.tgz",
837
+ "integrity": "sha512-JRdaprkZjZ6EY4WVwsZaEjPUj9W9vqlSaFDm4oD+IbwlY4GjAXuUQK6skKcvVyoOsSTvJp/CaveSws2FiWUp9Q==",
838
  "license": "Apache-2.0",
839
  "dependencies": {
840
+ "@aws-sdk/core": "3.927.0",
841
+ "@aws-sdk/nested-clients": "3.927.0",
842
  "@aws-sdk/types": "3.922.0",
843
  "@smithy/property-provider": "^4.2.4",
844
  "@smithy/shared-ini-file-loader": "^4.3.4",
 
903
  }
904
  },
905
  "node_modules/@aws-sdk/util-user-agent-node": {
906
+ "version": "3.927.0",
907
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.927.0.tgz",
908
+ "integrity": "sha512-5Ty+29jBTHg1mathEhLJavzA7A7vmhephRYGenFzo8rApLZh+c+MCAqjddSjdDzcf5FH+ydGGnIrj4iIfbZIMQ==",
909
  "license": "Apache-2.0",
910
  "dependencies": {
911
+ "@aws-sdk/middleware-user-agent": "3.927.0",
912
  "@aws-sdk/types": "3.922.0",
913
  "@smithy/node-config-provider": "^4.3.4",
914
  "@smithy/types": "^4.8.1",
 
2422
  "@jridgewell/sourcemap-codec": "^1.4.14"
2423
  }
2424
  },
2425
+ "node_modules/@modelcontextprotocol/sdk": {
2426
+ "version": "1.21.1",
2427
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.21.1.tgz",
2428
+ "integrity": "sha512-UyLFcJLDvUuZbGnaQqXFT32CpPpGj7VS19roLut6gkQVhb439xUzYWbsUvdI3ZPL+2hnFosuugtYWE0Mcs1rmQ==",
2429
+ "license": "MIT",
2430
+ "dependencies": {
2431
+ "ajv": "^8.17.1",
2432
+ "ajv-formats": "^3.0.1",
2433
+ "content-type": "^1.0.5",
2434
+ "cors": "^2.8.5",
2435
+ "cross-spawn": "^7.0.5",
2436
+ "eventsource": "^3.0.2",
2437
+ "eventsource-parser": "^3.0.0",
2438
+ "express": "^5.0.1",
2439
+ "express-rate-limit": "^7.5.0",
2440
+ "pkce-challenge": "^5.0.0",
2441
+ "raw-body": "^3.0.0",
2442
+ "zod": "^3.23.8",
2443
+ "zod-to-json-schema": "^3.24.1"
2444
+ },
2445
+ "engines": {
2446
+ "node": ">=18"
2447
+ },
2448
+ "peerDependencies": {
2449
+ "@cfworker/json-schema": "^4.1.1"
2450
+ },
2451
+ "peerDependenciesMeta": {
2452
+ "@cfworker/json-schema": {
2453
+ "optional": true
2454
+ }
2455
+ }
2456
+ },
2457
+ "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": {
2458
+ "version": "8.17.1",
2459
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
2460
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
2461
+ "license": "MIT",
2462
+ "dependencies": {
2463
+ "fast-deep-equal": "^3.1.3",
2464
+ "fast-uri": "^3.0.1",
2465
+ "json-schema-traverse": "^1.0.0",
2466
+ "require-from-string": "^2.0.2"
2467
+ },
2468
+ "funding": {
2469
+ "type": "github",
2470
+ "url": "https://github.com/sponsors/epoberezkin"
2471
+ }
2472
+ },
2473
+ "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": {
2474
+ "version": "1.0.0",
2475
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
2476
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
2477
+ "license": "MIT"
2478
+ },
2479
  "node_modules/@mongodb-js/saslprep": {
2480
  "version": "1.2.2",
2481
  "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.2.tgz",
 
4583
  "node": ">=6.5"
4584
  }
4585
  },
4586
+ "node_modules/accepts": {
4587
+ "version": "2.0.0",
4588
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
4589
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
4590
+ "license": "MIT",
4591
+ "dependencies": {
4592
+ "mime-types": "^3.0.0",
4593
+ "negotiator": "^1.0.0"
4594
+ },
4595
+ "engines": {
4596
+ "node": ">= 0.6"
4597
+ }
4598
+ },
4599
+ "node_modules/accepts/node_modules/mime-db": {
4600
+ "version": "1.54.0",
4601
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
4602
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
4603
+ "license": "MIT",
4604
+ "engines": {
4605
+ "node": ">= 0.6"
4606
+ }
4607
+ },
4608
+ "node_modules/accepts/node_modules/mime-types": {
4609
+ "version": "3.0.1",
4610
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
4611
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
4612
+ "license": "MIT",
4613
+ "dependencies": {
4614
+ "mime-db": "^1.54.0"
4615
+ },
4616
+ "engines": {
4617
+ "node": ">= 0.6"
4618
+ }
4619
+ },
4620
  "node_modules/acorn": {
4621
  "version": "8.14.1",
4622
  "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
 
4678
  "url": "https://github.com/sponsors/epoberezkin"
4679
  }
4680
  },
4681
+ "node_modules/ajv-formats": {
4682
+ "version": "3.0.1",
4683
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
4684
+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
4685
+ "license": "MIT",
4686
+ "dependencies": {
4687
+ "ajv": "^8.0.0"
4688
+ },
4689
+ "peerDependencies": {
4690
+ "ajv": "^8.0.0"
4691
+ },
4692
+ "peerDependenciesMeta": {
4693
+ "ajv": {
4694
+ "optional": true
4695
+ }
4696
+ }
4697
+ },
4698
+ "node_modules/ajv-formats/node_modules/ajv": {
4699
+ "version": "8.17.1",
4700
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
4701
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
4702
+ "license": "MIT",
4703
+ "dependencies": {
4704
+ "fast-deep-equal": "^3.1.3",
4705
+ "fast-uri": "^3.0.1",
4706
+ "json-schema-traverse": "^1.0.0",
4707
+ "require-from-string": "^2.0.2"
4708
+ },
4709
+ "funding": {
4710
+ "type": "github",
4711
+ "url": "https://github.com/sponsors/epoberezkin"
4712
+ }
4713
+ },
4714
+ "node_modules/ajv-formats/node_modules/json-schema-traverse": {
4715
+ "version": "1.0.0",
4716
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
4717
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
4718
+ "license": "MIT"
4719
+ },
4720
  "node_modules/ansi-escapes": {
4721
  "version": "7.0.0",
4722
  "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz",
 
4996
  "svelte": "^5.33.0"
4997
  }
4998
  },
4999
+ "node_modules/body-parser": {
5000
+ "version": "2.2.0",
5001
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
5002
+ "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
5003
+ "license": "MIT",
5004
+ "dependencies": {
5005
+ "bytes": "^3.1.2",
5006
+ "content-type": "^1.0.5",
5007
+ "debug": "^4.4.0",
5008
+ "http-errors": "^2.0.0",
5009
+ "iconv-lite": "^0.6.3",
5010
+ "on-finished": "^2.4.1",
5011
+ "qs": "^6.14.0",
5012
+ "raw-body": "^3.0.0",
5013
+ "type-is": "^2.0.0"
5014
+ },
5015
+ "engines": {
5016
+ "node": ">=18"
5017
+ }
5018
+ },
5019
  "node_modules/bowser": {
5020
  "version": "2.12.1",
5021
  "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz",
 
5134
  "node": "*"
5135
  }
5136
  },
5137
+ "node_modules/bytes": {
5138
+ "version": "3.1.2",
5139
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
5140
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
5141
+ "license": "MIT",
5142
+ "engines": {
5143
+ "node": ">= 0.8"
5144
+ }
5145
+ },
5146
  "node_modules/cac": {
5147
  "version": "6.7.14",
5148
  "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
 
5165
  "node": ">= 0.4"
5166
  }
5167
  },
5168
+ "node_modules/call-bound": {
5169
+ "version": "1.0.4",
5170
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
5171
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
5172
+ "license": "MIT",
5173
+ "dependencies": {
5174
+ "call-bind-apply-helpers": "^1.0.2",
5175
+ "get-intrinsic": "^1.3.0"
5176
+ },
5177
+ "engines": {
5178
+ "node": ">= 0.4"
5179
+ },
5180
+ "funding": {
5181
+ "url": "https://github.com/sponsors/ljharb"
5182
+ }
5183
+ },
5184
  "node_modules/callsites": {
5185
  "version": "3.1.0",
5186
  "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
 
5428
  "dev": true,
5429
  "license": "MIT"
5430
  },
5431
+ "node_modules/content-disposition": {
5432
+ "version": "1.0.0",
5433
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
5434
+ "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
5435
+ "license": "MIT",
5436
+ "dependencies": {
5437
+ "safe-buffer": "5.2.1"
5438
+ },
5439
+ "engines": {
5440
+ "node": ">= 0.6"
5441
+ }
5442
+ },
5443
+ "node_modules/content-type": {
5444
+ "version": "1.0.5",
5445
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
5446
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
5447
+ "license": "MIT",
5448
+ "engines": {
5449
+ "node": ">= 0.6"
5450
+ }
5451
+ },
5452
  "node_modules/cookie": {
5453
  "version": "0.6.0",
5454
  "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
 
5459
  "node": ">= 0.6"
5460
  }
5461
  },
5462
+ "node_modules/cookie-signature": {
5463
+ "version": "1.2.2",
5464
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
5465
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
5466
+ "license": "MIT",
5467
+ "engines": {
5468
+ "node": ">=6.6.0"
5469
+ }
5470
+ },
5471
  "node_modules/copy-anything": {
5472
  "version": "3.0.5",
5473
  "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
 
5484
  "url": "https://github.com/sponsors/mesqueeb"
5485
  }
5486
  },
5487
+ "node_modules/cors": {
5488
+ "version": "2.8.5",
5489
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
5490
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
5491
+ "license": "MIT",
5492
+ "dependencies": {
5493
+ "object-assign": "^4",
5494
+ "vary": "^1"
5495
+ },
5496
+ "engines": {
5497
+ "node": ">= 0.10"
5498
+ }
5499
+ },
5500
  "node_modules/cross-spawn": {
5501
  "version": "7.0.6",
5502
  "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
 
5664
  "node": ">=0.4.0"
5665
  }
5666
  },
5667
+ "node_modules/depd": {
5668
+ "version": "2.0.0",
5669
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
5670
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
5671
+ "license": "MIT",
5672
+ "engines": {
5673
+ "node": ">= 0.8"
5674
+ }
5675
+ },
5676
  "node_modules/dequal": {
5677
  "version": "2.0.3",
5678
  "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
 
5798
  "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
5799
  "license": "MIT"
5800
  },
5801
+ "node_modules/ee-first": {
5802
+ "version": "1.1.1",
5803
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
5804
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
5805
+ "license": "MIT"
5806
+ },
5807
  "node_modules/electron-to-chromium": {
5808
  "version": "1.5.165",
5809
  "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.165.tgz",
 
5847
  "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
5848
  "license": "MIT"
5849
  },
5850
+ "node_modules/encodeurl": {
5851
+ "version": "2.0.0",
5852
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
5853
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
5854
+ "license": "MIT",
5855
+ "engines": {
5856
+ "node": ">= 0.8"
5857
+ }
5858
+ },
5859
  "node_modules/end-of-stream": {
5860
  "version": "1.4.4",
5861
  "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
 
6283
  "node": ">=0.10.0"
6284
  }
6285
  },
6286
+ "node_modules/etag": {
6287
+ "version": "1.8.1",
6288
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
6289
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
6290
+ "license": "MIT",
6291
+ "engines": {
6292
+ "node": ">= 0.6"
6293
+ }
6294
+ },
6295
  "node_modules/event-target-shim": {
6296
  "version": "5.0.1",
6297
  "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
 
6316
  "node": ">=0.8.x"
6317
  }
6318
  },
6319
+ "node_modules/eventsource": {
6320
+ "version": "3.0.7",
6321
+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
6322
+ "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
6323
+ "license": "MIT",
6324
+ "dependencies": {
6325
+ "eventsource-parser": "^3.0.1"
6326
+ },
6327
+ "engines": {
6328
+ "node": ">=18.0.0"
6329
+ }
6330
+ },
6331
+ "node_modules/eventsource-parser": {
6332
+ "version": "3.0.6",
6333
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
6334
+ "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
6335
+ "license": "MIT",
6336
+ "engines": {
6337
+ "node": ">=18.0.0"
6338
+ }
6339
+ },
6340
  "node_modules/exact-mirror": {
6341
  "version": "0.1.2",
6342
  "resolved": "https://registry.npmjs.org/exact-mirror/-/exact-mirror-0.1.2.tgz",
 
6395
  "node": ">=12.0.0"
6396
  }
6397
  },
6398
+ "node_modules/express": {
6399
+ "version": "5.1.0",
6400
+ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
6401
+ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
6402
+ "license": "MIT",
6403
+ "dependencies": {
6404
+ "accepts": "^2.0.0",
6405
+ "body-parser": "^2.2.0",
6406
+ "content-disposition": "^1.0.0",
6407
+ "content-type": "^1.0.5",
6408
+ "cookie": "^0.7.1",
6409
+ "cookie-signature": "^1.2.1",
6410
+ "debug": "^4.4.0",
6411
+ "encodeurl": "^2.0.0",
6412
+ "escape-html": "^1.0.3",
6413
+ "etag": "^1.8.1",
6414
+ "finalhandler": "^2.1.0",
6415
+ "fresh": "^2.0.0",
6416
+ "http-errors": "^2.0.0",
6417
+ "merge-descriptors": "^2.0.0",
6418
+ "mime-types": "^3.0.0",
6419
+ "on-finished": "^2.4.1",
6420
+ "once": "^1.4.0",
6421
+ "parseurl": "^1.3.3",
6422
+ "proxy-addr": "^2.0.7",
6423
+ "qs": "^6.14.0",
6424
+ "range-parser": "^1.2.1",
6425
+ "router": "^2.2.0",
6426
+ "send": "^1.1.0",
6427
+ "serve-static": "^2.2.0",
6428
+ "statuses": "^2.0.1",
6429
+ "type-is": "^2.0.1",
6430
+ "vary": "^1.1.2"
6431
+ },
6432
+ "engines": {
6433
+ "node": ">= 18"
6434
+ },
6435
+ "funding": {
6436
+ "type": "opencollective",
6437
+ "url": "https://opencollective.com/express"
6438
+ }
6439
+ },
6440
+ "node_modules/express-rate-limit": {
6441
+ "version": "7.5.1",
6442
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
6443
+ "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
6444
+ "license": "MIT",
6445
+ "engines": {
6446
+ "node": ">= 16"
6447
+ },
6448
+ "funding": {
6449
+ "url": "https://github.com/sponsors/express-rate-limit"
6450
+ },
6451
+ "peerDependencies": {
6452
+ "express": ">= 4.11"
6453
+ }
6454
+ },
6455
+ "node_modules/express/node_modules/cookie": {
6456
+ "version": "0.7.2",
6457
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
6458
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
6459
+ "license": "MIT",
6460
+ "engines": {
6461
+ "node": ">= 0.6"
6462
+ }
6463
+ },
6464
+ "node_modules/express/node_modules/mime-db": {
6465
+ "version": "1.54.0",
6466
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
6467
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
6468
+ "license": "MIT",
6469
+ "engines": {
6470
+ "node": ">= 0.6"
6471
+ }
6472
+ },
6473
+ "node_modules/express/node_modules/mime-types": {
6474
+ "version": "3.0.1",
6475
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
6476
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
6477
+ "license": "MIT",
6478
+ "dependencies": {
6479
+ "mime-db": "^1.54.0"
6480
+ },
6481
+ "engines": {
6482
+ "node": ">= 0.6"
6483
+ }
6484
+ },
6485
  "node_modules/exsolve": {
6486
  "version": "1.0.5",
6487
  "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.5.tgz",
 
6505
  "version": "3.1.3",
6506
  "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
6507
  "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
 
6508
  "license": "MIT"
6509
  },
6510
  "node_modules/fast-fifo": {
 
6571
  "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
6572
  "license": "MIT"
6573
  },
6574
+ "node_modules/fast-uri": {
6575
+ "version": "3.1.0",
6576
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
6577
+ "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
6578
+ "funding": [
6579
+ {
6580
+ "type": "github",
6581
+ "url": "https://github.com/sponsors/fastify"
6582
+ },
6583
+ {
6584
+ "type": "opencollective",
6585
+ "url": "https://opencollective.com/fastify"
6586
+ }
6587
+ ],
6588
+ "license": "BSD-3-Clause"
6589
+ },
6590
  "node_modules/fast-xml-parser": {
6591
  "version": "5.2.5",
6592
  "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz",
 
6677
  "node": ">=8"
6678
  }
6679
  },
6680
+ "node_modules/finalhandler": {
6681
+ "version": "2.1.0",
6682
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
6683
+ "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
6684
+ "license": "MIT",
6685
+ "dependencies": {
6686
+ "debug": "^4.4.0",
6687
+ "encodeurl": "^2.0.0",
6688
+ "escape-html": "^1.0.3",
6689
+ "on-finished": "^2.4.1",
6690
+ "parseurl": "^1.3.3",
6691
+ "statuses": "^2.0.1"
6692
+ },
6693
+ "engines": {
6694
+ "node": ">= 0.8"
6695
+ }
6696
+ },
6697
  "node_modules/find-cache-dir": {
6698
  "version": "3.3.2",
6699
  "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
 
6823
  "node": ">= 12.20"
6824
  }
6825
  },
6826
+ "node_modules/forwarded": {
6827
+ "version": "0.2.0",
6828
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
6829
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
6830
+ "license": "MIT",
6831
+ "engines": {
6832
+ "node": ">= 0.6"
6833
+ }
6834
+ },
6835
  "node_modules/fraction.js": {
6836
  "version": "4.3.7",
6837
  "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
 
6845
  "url": "https://github.com/sponsors/rawify"
6846
  }
6847
  },
6848
+ "node_modules/fresh": {
6849
+ "version": "2.0.0",
6850
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
6851
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
6852
+ "license": "MIT",
6853
+ "engines": {
6854
+ "node": ">= 0.8"
6855
+ }
6856
+ },
6857
  "node_modules/fs.realpath": {
6858
  "version": "1.0.0",
6859
  "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
 
7173
  "node": ">=12"
7174
  }
7175
  },
7176
+ "node_modules/http-errors": {
7177
+ "version": "2.0.0",
7178
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
7179
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
7180
+ "license": "MIT",
7181
+ "dependencies": {
7182
+ "depd": "2.0.0",
7183
+ "inherits": "2.0.4",
7184
+ "setprototypeof": "1.2.0",
7185
+ "statuses": "2.0.1",
7186
+ "toidentifier": "1.0.1"
7187
+ },
7188
+ "engines": {
7189
+ "node": ">= 0.8"
7190
+ }
7191
+ },
7192
+ "node_modules/http-errors/node_modules/statuses": {
7193
+ "version": "2.0.1",
7194
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
7195
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
7196
+ "license": "MIT",
7197
+ "engines": {
7198
+ "node": ">= 0.8"
7199
+ }
7200
+ },
7201
  "node_modules/http-proxy-agent": {
7202
  "version": "5.0.0",
7203
  "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
 
7356
  "version": "2.0.4",
7357
  "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
7358
  "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
 
7359
  "license": "ISC"
7360
  },
7361
  "node_modules/inline-style-parser": {
 
7383
  "node": ">= 12"
7384
  }
7385
  },
7386
+ "node_modules/ipaddr.js": {
7387
+ "version": "1.9.1",
7388
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
7389
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
7390
+ "license": "MIT",
7391
+ "engines": {
7392
+ "node": ">= 0.10"
7393
+ }
7394
+ },
7395
  "node_modules/is-arrayish": {
7396
  "version": "0.3.2",
7397
  "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
 
7490
  "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
7491
  "license": "MIT"
7492
  },
7493
+ "node_modules/is-promise": {
7494
+ "version": "4.0.0",
7495
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
7496
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
7497
+ "license": "MIT"
7498
+ },
7499
  "node_modules/is-reference": {
7500
  "version": "1.2.1",
7501
  "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
 
8300
  "node": ">= 0.4"
8301
  }
8302
  },
8303
+ "node_modules/media-typer": {
8304
+ "version": "1.1.0",
8305
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
8306
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
8307
+ "license": "MIT",
8308
+ "engines": {
8309
+ "node": ">= 0.8"
8310
+ }
8311
+ },
8312
  "node_modules/memory-pager": {
8313
  "version": "1.5.0",
8314
  "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
 
8316
  "devOptional": true,
8317
  "license": "MIT"
8318
  },
8319
+ "node_modules/merge-descriptors": {
8320
+ "version": "2.0.0",
8321
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
8322
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
8323
+ "license": "MIT",
8324
+ "engines": {
8325
+ "node": ">=18"
8326
+ },
8327
+ "funding": {
8328
+ "url": "https://github.com/sponsors/sindresorhus"
8329
+ }
8330
+ },
8331
  "node_modules/merge-stream": {
8332
  "version": "2.0.0",
8333
  "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
 
8748
  "dev": true,
8749
  "license": "MIT"
8750
  },
8751
+ "node_modules/negotiator": {
8752
+ "version": "1.0.0",
8753
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
8754
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
8755
+ "license": "MIT",
8756
+ "engines": {
8757
+ "node": ">= 0.6"
8758
+ }
8759
+ },
8760
  "node_modules/neo-async": {
8761
  "version": "2.6.2",
8762
  "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
 
8919
  "node": ">= 6"
8920
  }
8921
  },
8922
+ "node_modules/object-inspect": {
8923
+ "version": "1.13.4",
8924
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
8925
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
8926
+ "license": "MIT",
8927
+ "engines": {
8928
+ "node": ">= 0.4"
8929
+ },
8930
+ "funding": {
8931
+ "url": "https://github.com/sponsors/ljharb"
8932
+ }
8933
+ },
8934
  "node_modules/object-stream": {
8935
  "version": "0.0.1",
8936
  "resolved": "https://registry.npmjs.org/object-stream/-/object-stream-0.0.1.tgz",
 
8957
  "node": ">=14.0.0"
8958
  }
8959
  },
8960
+ "node_modules/on-finished": {
8961
+ "version": "2.4.1",
8962
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
8963
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
8964
+ "license": "MIT",
8965
+ "dependencies": {
8966
+ "ee-first": "1.1.1"
8967
+ },
8968
+ "engines": {
8969
+ "node": ">= 0.8"
8970
+ }
8971
+ },
8972
  "node_modules/once": {
8973
  "version": "1.4.0",
8974
  "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
 
9212
  "url": "https://github.com/inikulin/parse5?sponsor=1"
9213
  }
9214
  },
9215
+ "node_modules/parseurl": {
9216
+ "version": "1.3.3",
9217
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
9218
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
9219
+ "license": "MIT",
9220
+ "engines": {
9221
+ "node": ">= 0.8"
9222
+ }
9223
+ },
9224
  "node_modules/path-exists": {
9225
  "version": "4.0.0",
9226
  "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
 
9278
  "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
9279
  "license": "ISC"
9280
  },
9281
+ "node_modules/path-to-regexp": {
9282
+ "version": "8.3.0",
9283
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
9284
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
9285
+ "license": "MIT",
9286
+ "funding": {
9287
+ "type": "opencollective",
9288
+ "url": "https://opencollective.com/express"
9289
+ }
9290
+ },
9291
  "node_modules/path-type": {
9292
  "version": "4.0.0",
9293
  "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
 
9443
  "node": ">= 6"
9444
  }
9445
  },
9446
+ "node_modules/pkce-challenge": {
9447
+ "version": "5.0.0",
9448
+ "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
9449
+ "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==",
9450
+ "license": "MIT",
9451
+ "engines": {
9452
+ "node": ">=16.20.0"
9453
+ }
9454
+ },
9455
  "node_modules/pkg-dir": {
9456
  "version": "4.2.0",
9457
  "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
 
9988
  "node": "^16 || ^18 || >=20"
9989
  }
9990
  },
9991
+ "node_modules/proxy-addr": {
9992
+ "version": "2.0.7",
9993
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
9994
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
9995
+ "license": "MIT",
9996
+ "dependencies": {
9997
+ "forwarded": "0.2.0",
9998
+ "ipaddr.js": "1.9.1"
9999
+ },
10000
+ "engines": {
10001
+ "node": ">= 0.10"
10002
+ }
10003
+ },
10004
  "node_modules/psl": {
10005
  "version": "1.15.0",
10006
  "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
 
10043
  "teleport": ">=0.2.0"
10044
  }
10045
  },
10046
+ "node_modules/qs": {
10047
+ "version": "6.14.0",
10048
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
10049
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
10050
+ "license": "BSD-3-Clause",
10051
+ "dependencies": {
10052
+ "side-channel": "^1.1.0"
10053
+ },
10054
+ "engines": {
10055
+ "node": ">=0.6"
10056
+ },
10057
+ "funding": {
10058
+ "url": "https://github.com/sponsors/ljharb"
10059
+ }
10060
+ },
10061
  "node_modules/quansync": {
10062
  "version": "0.2.10",
10063
  "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz",
 
10107
  "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
10108
  "license": "MIT"
10109
  },
10110
+ "node_modules/range-parser": {
10111
+ "version": "1.2.1",
10112
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
10113
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
10114
+ "license": "MIT",
10115
+ "engines": {
10116
+ "node": ">= 0.6"
10117
+ }
10118
+ },
10119
+ "node_modules/raw-body": {
10120
+ "version": "3.0.1",
10121
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
10122
+ "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
10123
+ "license": "MIT",
10124
+ "dependencies": {
10125
+ "bytes": "3.1.2",
10126
+ "http-errors": "2.0.0",
10127
+ "iconv-lite": "0.7.0",
10128
+ "unpipe": "1.0.0"
10129
+ },
10130
+ "engines": {
10131
+ "node": ">= 0.10"
10132
+ }
10133
+ },
10134
+ "node_modules/raw-body/node_modules/iconv-lite": {
10135
+ "version": "0.7.0",
10136
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
10137
+ "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
10138
+ "license": "MIT",
10139
+ "dependencies": {
10140
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
10141
+ },
10142
+ "engines": {
10143
+ "node": ">=0.10.0"
10144
+ },
10145
+ "funding": {
10146
+ "type": "opencollective",
10147
+ "url": "https://opencollective.com/express"
10148
+ }
10149
+ },
10150
  "node_modules/react-is": {
10151
  "version": "17.0.2",
10152
  "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
 
10202
  "node": ">= 12.13.0"
10203
  }
10204
  },
10205
+ "node_modules/require-from-string": {
10206
+ "version": "2.0.2",
10207
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
10208
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
10209
+ "license": "MIT",
10210
+ "engines": {
10211
+ "node": ">=0.10.0"
10212
+ }
10213
+ },
10214
  "node_modules/requires-port": {
10215
  "version": "1.0.0",
10216
  "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
 
10356
  "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
10357
  "license": "MIT"
10358
  },
10359
+ "node_modules/router": {
10360
+ "version": "2.2.0",
10361
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
10362
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
10363
+ "license": "MIT",
10364
+ "dependencies": {
10365
+ "debug": "^4.4.0",
10366
+ "depd": "^2.0.0",
10367
+ "is-promise": "^4.0.0",
10368
+ "parseurl": "^1.3.3",
10369
+ "path-to-regexp": "^8.0.0"
10370
+ },
10371
+ "engines": {
10372
+ "node": ">= 18"
10373
+ }
10374
+ },
10375
  "node_modules/rrweb-cssom": {
10376
  "version": "0.6.0",
10377
  "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz",
 
10533
  "node": ">=10"
10534
  }
10535
  },
10536
+ "node_modules/send": {
10537
+ "version": "1.2.0",
10538
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
10539
+ "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
10540
+ "license": "MIT",
10541
+ "dependencies": {
10542
+ "debug": "^4.3.5",
10543
+ "encodeurl": "^2.0.0",
10544
+ "escape-html": "^1.0.3",
10545
+ "etag": "^1.8.1",
10546
+ "fresh": "^2.0.0",
10547
+ "http-errors": "^2.0.0",
10548
+ "mime-types": "^3.0.1",
10549
+ "ms": "^2.1.3",
10550
+ "on-finished": "^2.4.1",
10551
+ "range-parser": "^1.2.1",
10552
+ "statuses": "^2.0.1"
10553
+ },
10554
+ "engines": {
10555
+ "node": ">= 18"
10556
+ }
10557
+ },
10558
+ "node_modules/send/node_modules/mime-db": {
10559
+ "version": "1.54.0",
10560
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
10561
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
10562
+ "license": "MIT",
10563
+ "engines": {
10564
+ "node": ">= 0.6"
10565
+ }
10566
+ },
10567
+ "node_modules/send/node_modules/mime-types": {
10568
+ "version": "3.0.1",
10569
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
10570
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
10571
+ "license": "MIT",
10572
+ "dependencies": {
10573
+ "mime-db": "^1.54.0"
10574
+ },
10575
+ "engines": {
10576
+ "node": ">= 0.6"
10577
+ }
10578
+ },
10579
+ "node_modules/serve-static": {
10580
+ "version": "2.2.0",
10581
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
10582
+ "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
10583
+ "license": "MIT",
10584
+ "dependencies": {
10585
+ "encodeurl": "^2.0.0",
10586
+ "escape-html": "^1.0.3",
10587
+ "parseurl": "^1.3.3",
10588
+ "send": "^1.2.0"
10589
+ },
10590
+ "engines": {
10591
+ "node": ">= 18"
10592
+ }
10593
+ },
10594
  "node_modules/set-cookie-parser": {
10595
  "version": "2.7.1",
10596
  "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
 
10598
  "devOptional": true,
10599
  "license": "MIT"
10600
  },
10601
+ "node_modules/setprototypeof": {
10602
+ "version": "1.2.0",
10603
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
10604
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
10605
+ "license": "ISC"
10606
+ },
10607
  "node_modules/sharp": {
10608
  "version": "0.33.5",
10609
  "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
 
10664
  "node": ">=8"
10665
  }
10666
  },
10667
+ "node_modules/side-channel": {
10668
+ "version": "1.1.0",
10669
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
10670
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
10671
+ "license": "MIT",
10672
+ "dependencies": {
10673
+ "es-errors": "^1.3.0",
10674
+ "object-inspect": "^1.13.3",
10675
+ "side-channel-list": "^1.0.0",
10676
+ "side-channel-map": "^1.0.1",
10677
+ "side-channel-weakmap": "^1.0.2"
10678
+ },
10679
+ "engines": {
10680
+ "node": ">= 0.4"
10681
+ },
10682
+ "funding": {
10683
+ "url": "https://github.com/sponsors/ljharb"
10684
+ }
10685
+ },
10686
+ "node_modules/side-channel-list": {
10687
+ "version": "1.0.0",
10688
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
10689
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
10690
+ "license": "MIT",
10691
+ "dependencies": {
10692
+ "es-errors": "^1.3.0",
10693
+ "object-inspect": "^1.13.3"
10694
+ },
10695
+ "engines": {
10696
+ "node": ">= 0.4"
10697
+ },
10698
+ "funding": {
10699
+ "url": "https://github.com/sponsors/ljharb"
10700
+ }
10701
+ },
10702
+ "node_modules/side-channel-map": {
10703
+ "version": "1.0.1",
10704
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
10705
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
10706
+ "license": "MIT",
10707
+ "dependencies": {
10708
+ "call-bound": "^1.0.2",
10709
+ "es-errors": "^1.3.0",
10710
+ "get-intrinsic": "^1.2.5",
10711
+ "object-inspect": "^1.13.3"
10712
+ },
10713
+ "engines": {
10714
+ "node": ">= 0.4"
10715
+ },
10716
+ "funding": {
10717
+ "url": "https://github.com/sponsors/ljharb"
10718
+ }
10719
+ },
10720
+ "node_modules/side-channel-weakmap": {
10721
+ "version": "1.0.2",
10722
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
10723
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
10724
+ "license": "MIT",
10725
+ "dependencies": {
10726
+ "call-bound": "^1.0.2",
10727
+ "es-errors": "^1.3.0",
10728
+ "get-intrinsic": "^1.2.5",
10729
+ "object-inspect": "^1.13.3",
10730
+ "side-channel-map": "^1.0.1"
10731
+ },
10732
+ "engines": {
10733
+ "node": ">= 0.4"
10734
+ },
10735
+ "funding": {
10736
+ "url": "https://github.com/sponsors/ljharb"
10737
+ }
10738
+ },
10739
  "node_modules/siginfo": {
10740
  "version": "2.0.0",
10741
  "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
 
10903
  "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
10904
  "license": "MIT"
10905
  },
10906
+ "node_modules/statuses": {
10907
+ "version": "2.0.2",
10908
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
10909
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
10910
+ "license": "MIT",
10911
+ "engines": {
10912
+ "node": ">= 0.8"
10913
+ }
10914
+ },
10915
  "node_modules/std-env": {
10916
  "version": "3.9.0",
10917
  "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
 
11677
  "node": ">=8.0"
11678
  }
11679
  },
11680
+ "node_modules/toidentifier": {
11681
+ "version": "1.0.1",
11682
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
11683
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
11684
+ "license": "MIT",
11685
+ "engines": {
11686
+ "node": ">=0.6"
11687
+ }
11688
+ },
11689
  "node_modules/token-types": {
11690
  "version": "6.0.0",
11691
  "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz",
 
11790
  "url": "https://github.com/sponsors/sindresorhus"
11791
  }
11792
  },
11793
+ "node_modules/type-is": {
11794
+ "version": "2.0.1",
11795
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
11796
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
11797
+ "license": "MIT",
11798
+ "dependencies": {
11799
+ "content-type": "^1.0.5",
11800
+ "media-typer": "^1.1.0",
11801
+ "mime-types": "^3.0.0"
11802
+ },
11803
+ "engines": {
11804
+ "node": ">= 0.6"
11805
+ }
11806
+ },
11807
+ "node_modules/type-is/node_modules/mime-db": {
11808
+ "version": "1.54.0",
11809
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
11810
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
11811
+ "license": "MIT",
11812
+ "engines": {
11813
+ "node": ">= 0.6"
11814
+ }
11815
+ },
11816
+ "node_modules/type-is/node_modules/mime-types": {
11817
+ "version": "3.0.1",
11818
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
11819
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
11820
+ "license": "MIT",
11821
+ "dependencies": {
11822
+ "mime-db": "^1.54.0"
11823
+ },
11824
+ "engines": {
11825
+ "node": ">= 0.6"
11826
+ }
11827
+ },
11828
  "node_modules/typescript": {
11829
  "version": "5.8.3",
11830
  "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
 
11910
  "node": ">= 4.0.0"
11911
  }
11912
  },
11913
+ "node_modules/unpipe": {
11914
+ "version": "1.0.0",
11915
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
11916
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
11917
+ "license": "MIT",
11918
+ "engines": {
11919
+ "node": ">= 0.8"
11920
+ }
11921
+ },
11922
  "node_modules/unplugin": {
11923
  "version": "1.16.1",
11924
  "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz",
 
12051
  "integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow==",
12052
  "license": "MIT"
12053
  },
12054
+ "node_modules/vary": {
12055
+ "version": "1.1.2",
12056
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
12057
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
12058
+ "license": "MIT",
12059
+ "engines": {
12060
+ "node": ">= 0.8"
12061
+ }
12062
+ },
12063
  "node_modules/vite": {
12064
  "version": "6.3.5",
12065
  "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
 
12647
  "funding": {
12648
  "url": "https://github.com/sponsors/colinhacks"
12649
  }
12650
+ },
12651
+ "node_modules/zod-to-json-schema": {
12652
+ "version": "3.24.6",
12653
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
12654
+ "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
12655
+ "license": "ISC",
12656
+ "peerDependencies": {
12657
+ "zod": "^3.24.1"
12658
+ }
12659
  }
12660
  }
12661
  }
package.json CHANGED
@@ -70,6 +70,7 @@
70
  "@elysiajs/swagger": "^1.3.0",
71
  "@huggingface/hub": "^2.2.0",
72
  "@huggingface/inference": "^4.11.3",
 
73
  "@iconify-json/bi": "^1.1.21",
74
  "@resvg/resvg-js": "^2.6.2",
75
  "autoprefixer": "^10.4.14",
 
70
  "@elysiajs/swagger": "^1.3.0",
71
  "@huggingface/hub": "^2.2.0",
72
  "@huggingface/inference": "^4.11.3",
73
+ "@modelcontextprotocol/sdk": "^1.21.1",
74
  "@iconify-json/bi": "^1.1.21",
75
  "@resvg/resvg-js": "^2.6.2",
76
  "autoprefixer": "^10.4.14",
src/hooks.server.ts CHANGED
@@ -19,6 +19,7 @@ import { refreshConversationStats } from "$lib/jobs/refresh-conversation-stats";
19
  import { adminTokenManager } from "$lib/server/adminToken";
20
  import { isHostLocalhost } from "$lib/server/isURLLocal";
21
  import { MetricsServer } from "$lib/server/metrics";
 
22
 
23
  export const init: ServerInit = async () => {
24
  // Wait for config to be fully loaded
@@ -49,6 +50,9 @@ export const init: ServerInit = async () => {
49
  checkAndRunMigrations();
50
  refreshConversationStats();
51
 
 
 
 
52
  // Init AbortedGenerations refresh process
53
  AbortedGenerations.getInstance();
54
 
 
19
  import { adminTokenManager } from "$lib/server/adminToken";
20
  import { isHostLocalhost } from "$lib/server/isURLLocal";
21
  import { MetricsServer } from "$lib/server/metrics";
22
+ import { loadMcpServersOnStartup } from "$lib/server/mcp/registry";
23
 
24
  export const init: ServerInit = async () => {
25
  // Wait for config to be fully loaded
 
50
  checkAndRunMigrations();
51
  refreshConversationStats();
52
 
53
+ // Load MCP servers at startup
54
+ loadMcpServersOnStartup();
55
+
56
  // Init AbortedGenerations refresh process
57
  AbortedGenerations.getInstance();
58
 
src/lib/components/NavMenu.svelte CHANGED
@@ -28,6 +28,8 @@
28
  import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
29
  import { useAPIClient, handleResponse } from "$lib/APIClient";
30
  import { requireAuthUser } from "$lib/utils/auth";
 
 
31
 
32
  const publicConfig = usePublicConfig();
33
  const client = useAPIClient();
@@ -112,6 +114,7 @@
112
 
113
  let isDark = $state(false);
114
  let unsubscribeTheme: (() => void) | undefined;
 
115
 
116
  if (browser) {
117
  unsubscribeTheme = subscribeToTheme(({ isDark: nextIsDark }) => {
@@ -194,6 +197,22 @@
194
  >
195
  </a>
196
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  <span class="flex gap-1">
198
  <a
199
  href="{base}/settings/application"
@@ -219,3 +238,7 @@
219
  </button>
220
  </span>
221
  </div>
 
 
 
 
 
28
  import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
29
  import { useAPIClient, handleResponse } from "$lib/APIClient";
30
  import { requireAuthUser } from "$lib/utils/auth";
31
+ import { enabledServersCount } from "$lib/stores/mcpServers";
32
+ import MCPServerManager from "./mcp/MCPServerManager.svelte";
33
 
34
  const publicConfig = usePublicConfig();
35
  const client = useAPIClient();
 
114
 
115
  let isDark = $state(false);
116
  let unsubscribeTheme: (() => void) | undefined;
117
+ let showMcpModal = $state(false);
118
 
119
  if (browser) {
120
  unsubscribeTheme = subscribeToTheme(({ isDark: nextIsDark }) => {
 
197
  >
198
  </a>
199
 
200
+ {#if user?.username || user?.email}
201
+ <button
202
+ onclick={() => (showMcpModal = true)}
203
+ class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
204
+ >
205
+ MCP Servers
206
+ {#if $enabledServersCount > 0}
207
+ <span
208
+ class="ml-auto rounded-md bg-blue-600/10 px-1.5 py-0.5 text-xs text-blue-600 dark:bg-blue-600/20 dark:text-blue-400"
209
+ >
210
+ {$enabledServersCount}
211
+ </span>
212
+ {/if}
213
+ </button>
214
+ {/if}
215
+
216
  <span class="flex gap-1">
217
  <a
218
  href="{base}/settings/application"
 
238
  </button>
239
  </span>
240
  </div>
241
+
242
+ {#if showMcpModal}
243
+ <MCPServerManager onclose={() => (showMcpModal = false)} />
244
+ {/if}
src/lib/components/Switch.svelte CHANGED
@@ -27,7 +27,7 @@
27
  tabindex="0"
28
  onclick={toggle}
29
  onkeydown={onKeydown}
30
- class="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full bg-gray-300 p-1 shadow-inner ring-gray-400 peer-checked:bg-black hover:bg-gray-400 focus-visible:ring focus-visible:ring-offset-1 dark:bg-gray-600 dark:ring-gray-700 dark:peer-checked:bg-blue-600 dark:hover:bg-gray-500 peer-checked:[&>div]:translate-x-3.5"
31
  >
32
  <div class="h-3.5 w-3.5 rounded-full bg-white shadow-sm transition-transform"></div>
33
  </div>
 
27
  tabindex="0"
28
  onclick={toggle}
29
  onkeydown={onKeydown}
30
+ class="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full bg-gray-300 p-1 shadow-inner ring-gray-400 peer-checked:bg-blue-600 hover:bg-gray-400 peer-checked:hover:bg-blue-600 focus-visible:ring focus-visible:ring-offset-1 dark:bg-gray-600 dark:ring-gray-700 dark:hover:bg-gray-500 dark:peer-checked:hover:bg-blue-600 peer-checked:[&>div]:translate-x-3.5"
31
  >
32
  <div class="h-3.5 w-3.5 rounded-full bg-white shadow-sm transition-transform"></div>
33
  </div>
src/lib/components/chat/ChatInput.svelte CHANGED
@@ -10,11 +10,21 @@
10
  import CarbonUpload from "~icons/carbon/upload";
11
  import CarbonLink from "~icons/carbon/link";
12
  import CarbonChevronRight from "~icons/carbon/chevron-right";
 
13
  import UrlFetchModal from "./UrlFetchModal.svelte";
14
  import { TEXT_MIME_ALLOWLIST, IMAGE_MIME_ALLOWLIST_DEFAULT } from "$lib/constants/mime";
 
 
15
 
16
  import { isVirtualKeyboard } from "$lib/utils/isVirtualKeyboard";
17
  import { requireAuthUser } from "$lib/utils/auth";
 
 
 
 
 
 
 
18
 
19
  interface Props {
20
  files?: File[];
@@ -25,6 +35,8 @@
25
  disabled?: boolean;
26
  // tools removed
27
  modelIsMultimodal?: boolean;
 
 
28
  children?: import("svelte").Snippet;
29
  onPaste?: (e: ClipboardEvent) => void;
30
  focused?: boolean;
@@ -40,6 +52,7 @@
40
  disabled = false,
41
 
42
  modelIsMultimodal = false,
 
43
  children,
44
  onPaste,
45
  focused = $bindable(false),
@@ -62,6 +75,7 @@
62
 
63
  let fileInputEl: HTMLInputElement | undefined = $state();
64
  let isUrlModalOpen = $state(false);
 
65
 
66
  function openPickerWithAccept(accept: string) {
67
  if (!fileInputEl) return;
@@ -243,10 +257,13 @@
243
  </DropdownMenu.Trigger>
244
  <DropdownMenu.Portal>
245
  <DropdownMenu.Content
246
- class="z-50 rounded-xl border border-gray-200 bg-white/95 p-1 text-gray-800 shadow-lg backdrop-blur supports-[backdrop-filter]:bg-white/80 dark:border-gray-700/60 dark:bg-gray-800/95 dark:text-gray-100 dark:supports-[backdrop-filter]:bg-gray-800/80"
247
  side="top"
248
  sideOffset={8}
249
  align="start"
 
 
 
250
  >
251
  {#if modelIsMultimodal}
252
  <DropdownMenu.Item
@@ -271,8 +288,11 @@
271
  </div>
272
  </DropdownMenu.SubTrigger>
273
  <DropdownMenu.SubContent
274
- class="z-50 rounded-xl border border-gray-200 bg-white/95 p-1 text-gray-800 shadow-lg backdrop-blur supports-[backdrop-filter]:bg-white/80 dark:border-gray-700/60 dark:bg-gray-800/95 dark:text-gray-100 dark:supports-[backdrop-filter]:bg-gray-800/80"
275
  sideOffset={10}
 
 
 
276
  >
277
  <DropdownMenu.Item
278
  class="flex h-8 select-none items-center gap-1 rounded-md px-2 text-sm text-gray-700 data-[highlighted]:bg-gray-100 focus-visible:outline-none dark:text-gray-200 dark:data-[highlighted]:bg-white/10"
@@ -290,9 +310,105 @@
290
  </DropdownMenu.Item>
291
  </DropdownMenu.SubContent>
292
  </DropdownMenu.Sub>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  </DropdownMenu.Content>
294
  </DropdownMenu.Portal>
295
  </DropdownMenu.Root>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  </div>
297
  {/if}
298
  </div>
@@ -304,6 +420,10 @@
304
  acceptMimeTypes={mimeTypes}
305
  onfiles={handleFetchedFiles}
306
  />
 
 
 
 
307
  </div>
308
 
309
  <style lang="postcss">
 
10
  import CarbonUpload from "~icons/carbon/upload";
11
  import CarbonLink from "~icons/carbon/link";
12
  import CarbonChevronRight from "~icons/carbon/chevron-right";
13
+ import CarbonClose from "~icons/carbon/close";
14
  import UrlFetchModal from "./UrlFetchModal.svelte";
15
  import { TEXT_MIME_ALLOWLIST, IMAGE_MIME_ALLOWLIST_DEFAULT } from "$lib/constants/mime";
16
+ import MCPServerManager from "$lib/components/mcp/MCPServerManager.svelte";
17
+ import IconMCP from "$lib/components/icons/IconMCP.svelte";
18
 
19
  import { isVirtualKeyboard } from "$lib/utils/isVirtualKeyboard";
20
  import { requireAuthUser } from "$lib/utils/auth";
21
+ import {
22
+ enabledServersCount,
23
+ selectedServerIds,
24
+ allMcpServers,
25
+ toggleServer,
26
+ } from "$lib/stores/mcpServers";
27
+ import { getMcpServerFaviconUrl } from "$lib/utils/favicon";
28
 
29
  interface Props {
30
  files?: File[];
 
35
  disabled?: boolean;
36
  // tools removed
37
  modelIsMultimodal?: boolean;
38
+ // Whether the currently selected model supports tool calling (incl. overrides)
39
+ modelSupportsTools?: boolean;
40
  children?: import("svelte").Snippet;
41
  onPaste?: (e: ClipboardEvent) => void;
42
  focused?: boolean;
 
52
  disabled = false,
53
 
54
  modelIsMultimodal = false,
55
+ modelSupportsTools = true,
56
  children,
57
  onPaste,
58
  focused = $bindable(false),
 
75
 
76
  let fileInputEl: HTMLInputElement | undefined = $state();
77
  let isUrlModalOpen = $state(false);
78
+ let isMcpManagerOpen = $state(false);
79
 
80
  function openPickerWithAccept(accept: string) {
81
  if (!fileInputEl) return;
 
257
  </DropdownMenu.Trigger>
258
  <DropdownMenu.Portal>
259
  <DropdownMenu.Content
260
+ class="z-50 rounded-xl border border-gray-200 bg-white/95 p-1 text-gray-800 shadow-lg backdrop-blur dark:border-gray-700/60 dark:bg-gray-800/95 dark:text-gray-100"
261
  side="top"
262
  sideOffset={8}
263
  align="start"
264
+ trapFocus={false}
265
+ onCloseAutoFocus={(e) => e.preventDefault()}
266
+ interactOutsideBehavior="defer-otherwise-close"
267
  >
268
  {#if modelIsMultimodal}
269
  <DropdownMenu.Item
 
288
  </div>
289
  </DropdownMenu.SubTrigger>
290
  <DropdownMenu.SubContent
291
+ class="z-50 rounded-xl border border-gray-200 bg-white/95 p-1 text-gray-800 shadow-lg backdrop-blur dark:border-gray-700/60 dark:bg-gray-800/95 dark:text-gray-100"
292
  sideOffset={10}
293
+ trapFocus={false}
294
+ onCloseAutoFocus={(e) => e.preventDefault()}
295
+ interactOutsideBehavior="defer-otherwise-close"
296
  >
297
  <DropdownMenu.Item
298
  class="flex h-8 select-none items-center gap-1 rounded-md px-2 text-sm text-gray-700 data-[highlighted]:bg-gray-100 focus-visible:outline-none dark:text-gray-200 dark:data-[highlighted]:bg-white/10"
 
310
  </DropdownMenu.Item>
311
  </DropdownMenu.SubContent>
312
  </DropdownMenu.Sub>
313
+
314
+ <!-- MCP Servers submenu -->
315
+ <DropdownMenu.Sub>
316
+ <DropdownMenu.SubTrigger
317
+ class="flex h-8 select-none items-center gap-1 rounded-md px-2 text-sm text-gray-700 data-[highlighted]:bg-gray-100 data-[state=open]:bg-gray-100 focus-visible:outline-none dark:text-gray-200 dark:data-[highlighted]:bg-white/10 dark:data-[state=open]:bg-white/10"
318
+ >
319
+ <div class="flex items-center gap-1">
320
+ <IconMCP classNames="size-4 opacity-90 dark:opacity-80" />
321
+ MCP Servers
322
+ </div>
323
+ <div class="ml-auto flex items-center">
324
+ <CarbonChevronRight class="size-4 opacity-70 dark:opacity-80" />
325
+ </div>
326
+ </DropdownMenu.SubTrigger>
327
+ <DropdownMenu.SubContent
328
+ class="z-50 rounded-xl border border-gray-200 bg-white/95 p-1 text-gray-800 shadow-lg backdrop-blur dark:border-gray-700/60 dark:bg-gray-800/95 dark:text-gray-100"
329
+ sideOffset={10}
330
+ trapFocus={false}
331
+ onCloseAutoFocus={(e) => e.preventDefault()}
332
+ interactOutsideBehavior="defer-otherwise-close"
333
+ >
334
+ {#each $allMcpServers as server (server.id)}
335
+ <DropdownMenu.CheckboxItem
336
+ checked={$selectedServerIds.has(server.id)}
337
+ onCheckedChange={() => toggleServer(server.id)}
338
+ closeOnSelect={false}
339
+ class="flex h-9 select-none items-center gap-2 rounded-md px-2 text-sm leading-none text-gray-800 data-[highlighted]:bg-gray-100 focus-visible:outline-none dark:text-gray-100 dark:data-[highlighted]:bg-white/10"
340
+ >
341
+ {#snippet children({ checked })}
342
+ <img
343
+ src={getMcpServerFaviconUrl(server.url)}
344
+ alt=""
345
+ class="size-4 flex-shrink-0 rounded"
346
+ />
347
+ <span class="max-w-52 truncate py-1">{server.name}</span>
348
+ <div class="ml-auto flex items-center">
349
+ <!-- Toggle visual -->
350
+ <span
351
+ class={[
352
+ "relative mt-px flex h-4 w-7 items-center self-center rounded-full transition-colors",
353
+ checked ? "bg-blue-600/80" : "bg-gray-300 dark:bg-gray-700",
354
+ ]}
355
+ >
356
+ <span
357
+ class={[
358
+ "block size-3 translate-x-0.5 rounded-full bg-white shadow transition-transform",
359
+ checked ? "translate-x-[14px]" : "translate-x-0.5",
360
+ ]}
361
+ ></span>
362
+ </span>
363
+ </div>
364
+ {/snippet}
365
+ </DropdownMenu.CheckboxItem>
366
+ {/each}
367
+
368
+ {#if $allMcpServers.length > 0}
369
+ <DropdownMenu.Separator class="my-1 h-px bg-gray-200 dark:bg-gray-700/60" />
370
+ {/if}
371
+ <DropdownMenu.Item
372
+ class="flex h-8 select-none items-center gap-1 rounded-md px-2 text-sm text-gray-700 data-[highlighted]:bg-gray-100 focus-visible:outline-none dark:text-gray-200 dark:data-[highlighted]:bg-white/10"
373
+ onSelect={() => (isMcpManagerOpen = true)}
374
+ >
375
+ Manage MCP Servers
376
+ </DropdownMenu.Item>
377
+ </DropdownMenu.SubContent>
378
+ </DropdownMenu.Sub>
379
  </DropdownMenu.Content>
380
  </DropdownMenu.Portal>
381
  </DropdownMenu.Root>
382
+
383
+ {#if $enabledServersCount > 0}
384
+ <div
385
+ class="ml-2 inline-flex h-7 items-center gap-1.5 rounded-full border border-blue-500/10 bg-blue-600/10 pl-3 pr-1 text-xs font-semibold text-blue-700 dark:bg-blue-600/20 dark:text-blue-400"
386
+ class:grayscale={!modelSupportsTools}
387
+ class:opacity-60={!modelSupportsTools}
388
+ class:cursor-help={!modelSupportsTools}
389
+ title={modelSupportsTools
390
+ ? "MCP servers enabled"
391
+ : "Current model doesn’t support tools"}
392
+ >
393
+ <button
394
+ class="cursor-pointer select-none bg-transparent p-0 leading-none text-current focus:outline-none"
395
+ type="button"
396
+ title="Manage MCP Servers"
397
+ onclick={() => (isMcpManagerOpen = true)}
398
+ class:line-through={!modelSupportsTools}
399
+ >
400
+ MCP ({$enabledServersCount})
401
+ </button>
402
+ <button
403
+ class="grid size-5 place-items-center rounded-full bg-blue-600/15 text-blue-700 transition-colors hover:bg-blue-600/25 dark:bg-blue-600/25 dark:text-blue-300 dark:hover:bg-blue-600/35"
404
+ aria-label="Disable all MCP servers"
405
+ onclick={() => selectedServerIds.set(new Set())}
406
+ type="button"
407
+ >
408
+ <CarbonClose class="size-3.5" />
409
+ </button>
410
+ </div>
411
+ {/if}
412
  </div>
413
  {/if}
414
  </div>
 
420
  acceptMimeTypes={mimeTypes}
421
  onfiles={handleFetchedFiles}
422
  />
423
+
424
+ {#if isMcpManagerOpen}
425
+ <MCPServerManager onclose={() => (isMcpManagerOpen = false)} />
426
+ {/if}
427
  </div>
428
 
429
  <style lang="postcss">
src/lib/components/chat/ChatMessage.svelte CHANGED
@@ -18,6 +18,8 @@
18
  import MessageAvatar from "./MessageAvatar.svelte";
19
  import { PROVIDERS_HUB_ORGS } from "@huggingface/inference";
20
  import { requireAuthUser } from "$lib/utils/auth";
 
 
21
 
22
  interface Props {
23
  message: Message;
@@ -77,6 +79,41 @@
77
  message.content.replace(THINK_BLOCK_REGEX, "").trim()
78
  );
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  $effect(() => {
81
  if (isCopied) {
82
  setTimeout(() => {
@@ -125,6 +162,27 @@
125
  </div>
126
  {/if}
127
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  <div bind:this={contentEl}>
129
  {#if isLast && loading && message.content.length === 0}
130
  <IconLoading classNames="loading inline ml-2 first:ml-0" />
@@ -148,7 +206,7 @@
148
  />
149
  {:else if part && part.trim().length > 0}
150
  <div
151
- class="prose max-w-none dark:prose-invert max-sm:prose-sm prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900"
152
  >
153
  <MarkdownRenderer content={part} loading={isLast && loading} />
154
  </div>
@@ -156,7 +214,7 @@
156
  {/each}
157
  {:else}
158
  <div
159
- class="prose max-w-none dark:prose-invert max-sm:prose-sm prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900"
160
  >
161
  <MarkdownRenderer content={message.content} loading={isLast && loading} />
162
  </div>
 
18
  import MessageAvatar from "./MessageAvatar.svelte";
19
  import { PROVIDERS_HUB_ORGS } from "@huggingface/inference";
20
  import { requireAuthUser } from "$lib/utils/auth";
21
+ import ToolUpdate from "./ToolUpdate.svelte";
22
+ import { isMessageToolUpdate } from "$lib/utils/messageUpdates";
23
 
24
  interface Props {
25
  message: Message;
 
79
  message.content.replace(THINK_BLOCK_REGEX, "").trim()
80
  );
81
 
82
+ // Group tool updates (if any) by uuid for display
83
+ let toolUpdateGroups = $derived.by(() => {
84
+ const groups: Record<string, import("$lib/types/MessageUpdate").MessageToolUpdate[]> = {};
85
+ for (const u of message.updates ?? []) {
86
+ if (!isMessageToolUpdate(u)) continue;
87
+ (groups[u.uuid] ||= []).push(u);
88
+ }
89
+ return groups;
90
+ });
91
+ let hasToolUpdates = $derived(Object.keys(toolUpdateGroups).length > 0);
92
+
93
+ // Flatten to ordered array and keep a navigation index (defaults to last)
94
+ let toolGroups = $derived(Object.values(toolUpdateGroups));
95
+ let toolNavIndex = $state(0);
96
+ // Auto-follow newest tool group while streaming until user navigates manually
97
+ let toolAutoFollowLatest = $state(true);
98
+ $effect(() => {
99
+ const len = toolGroups.length;
100
+ if (len === 0) {
101
+ toolNavIndex = 0;
102
+ return;
103
+ }
104
+ // Clamp if groups shrink or grow
105
+ if (toolNavIndex > len - 1) toolNavIndex = len - 1;
106
+ // While streaming, default to most recent group unless user navigated away
107
+ if (isLast && loading && toolAutoFollowLatest) toolNavIndex = len - 1;
108
+ });
109
+
110
+ // When streaming ends, re-enable auto-follow for the next turn
111
+ $effect(() => {
112
+ if (!loading) {
113
+ toolAutoFollowLatest = true;
114
+ }
115
+ });
116
+
117
  $effect(() => {
118
  if (isCopied) {
119
  setTimeout(() => {
 
162
  </div>
163
  {/if}
164
 
165
+ {#if hasToolUpdates}
166
+ {#if toolGroups.length}
167
+ {@const group = toolGroups[toolNavIndex]}
168
+ <ToolUpdate
169
+ tool={group}
170
+ {loading}
171
+ index={toolNavIndex}
172
+ total={toolGroups.length}
173
+ onprev={() => {
174
+ toolAutoFollowLatest = false;
175
+ toolNavIndex = Math.max(0, toolNavIndex - 1);
176
+ }}
177
+ onnext={() => {
178
+ toolNavIndex = Math.min(toolGroups.length - 1, toolNavIndex + 1);
179
+ // If user moves back to the newest group, resume auto-follow
180
+ toolAutoFollowLatest = toolNavIndex === toolGroups.length - 1;
181
+ }}
182
+ />
183
+ {/if}
184
+ {/if}
185
+
186
  <div bind:this={contentEl}>
187
  {#if isLast && loading && message.content.length === 0}
188
  <IconLoading classNames="loading inline ml-2 first:ml-0" />
 
206
  />
207
  {:else if part && part.trim().length > 0}
208
  <div
209
+ class="prose max-w-none dark:prose-invert max-sm:prose-sm prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 prose-img:my-0 prose-img:rounded-lg dark:prose-pre:bg-gray-900"
210
  >
211
  <MarkdownRenderer content={part} loading={isLast && loading} />
212
  </div>
 
214
  {/each}
215
  {:else}
216
  <div
217
+ class="prose max-w-none dark:prose-invert max-sm:prose-sm prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 prose-img:my-0 prose-img:rounded-lg dark:prose-pre:bg-gray-900"
218
  >
219
  <MarkdownRenderer content={message.content} loading={isLast && loading} />
220
  </div>
src/lib/components/chat/ChatWindow.svelte CHANGED
@@ -27,12 +27,20 @@
27
  import { routerExamples } from "$lib/constants/routerExamples";
28
  import type { RouterFollowUp, RouterExample } from "$lib/constants/routerExamples";
29
  import { shareModal } from "$lib/stores/shareModal";
 
30
 
31
  import { fly } from "svelte/transition";
32
  import { cubicInOut } from "svelte/easing";
33
 
34
  import { isVirtualKeyboard } from "$lib/utils/isVirtualKeyboard";
35
  import { requireAuthUser } from "$lib/utils/auth";
 
 
 
 
 
 
 
36
 
37
  interface Props {
38
  messages?: Message[];
@@ -163,6 +171,26 @@
163
  ? (streamingRouterMetadata.model.split("/").pop() ?? streamingRouterMetadata.model)
164
  : ""
165
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  let showRouterDetails = $state(false);
167
  let routerDetailsTimeout: ReturnType<typeof setTimeout> | undefined;
168
 
@@ -231,6 +259,12 @@
231
  let modelIsMultimodalOverride = $derived($settings.multimodalOverrides?.[currentModel.id]);
232
  let modelIsMultimodal = $derived((modelIsMultimodalOverride ?? currentModel.multimodal) === true);
233
 
 
 
 
 
 
 
234
  // Always allow common text-like files; add images only when model is multimodal
235
  import { TEXT_MIME_ALLOWLIST, IMAGE_MIME_ALLOWLIST_DEFAULT } from "$lib/constants/mime";
236
 
@@ -492,6 +526,7 @@
492
  {onPaste}
493
  disabled={isReadOnly || lastIsError}
494
  {modelIsMultimodal}
 
495
  bind:focused
496
  />
497
  {/if}
@@ -539,7 +574,16 @@
539
  }}
540
  >
541
  {#if models.find((m) => m.id === currentModel.id)}
542
- {#if !currentModel.isRouter || !loading}
 
 
 
 
 
 
 
 
 
543
  <a
544
  href="{base}/settings/{currentModel.id}"
545
  onclick={(e) => {
@@ -557,7 +601,7 @@
557
  {/if}
558
  <CarbonCaretDown class="-ml-0.5 text-xxs" />
559
  </a>
560
- {:else if showRouterDetails && streamingRouterMetadata}
561
  <div
562
  class="mr-2 flex items-center gap-1.5 whitespace-nowrap text-[.70rem] text-xs leading-none text-gray-400 dark:text-gray-400"
563
  >
 
27
  import { routerExamples } from "$lib/constants/routerExamples";
28
  import type { RouterFollowUp, RouterExample } from "$lib/constants/routerExamples";
29
  import { shareModal } from "$lib/stores/shareModal";
30
+ import CarbonTools from "~icons/carbon/tools";
31
 
32
  import { fly } from "svelte/transition";
33
  import { cubicInOut } from "svelte/easing";
34
 
35
  import { isVirtualKeyboard } from "$lib/utils/isVirtualKeyboard";
36
  import { requireAuthUser } from "$lib/utils/auth";
37
+ import { page } from "$app/state";
38
+ import {
39
+ isMessageToolCallUpdate,
40
+ isMessageToolErrorUpdate,
41
+ isMessageToolResultUpdate,
42
+ } from "$lib/utils/messageUpdates";
43
+ import type { ToolFront } from "$lib/types/Tool";
44
 
45
  interface Props {
46
  messages?: Message[];
 
171
  ? (streamingRouterMetadata.model.split("/").pop() ?? streamingRouterMetadata.model)
172
  : ""
173
  );
174
+
175
+ // Expose currently running tool call name (if any) from the streaming assistant message
176
+ const availableTools: ToolFront[] = $derived.by(
177
+ () => (page.data as { tools?: ToolFront[] } | undefined)?.tools ?? []
178
+ );
179
+ let streamingToolCallName = $derived.by(() => {
180
+ const updates = streamingAssistantMessage?.updates ?? [];
181
+ if (!updates.length) return null;
182
+ const done = new Set<string>();
183
+ for (const u of updates) {
184
+ if (isMessageToolResultUpdate(u) || isMessageToolErrorUpdate(u)) done.add(u.uuid);
185
+ }
186
+ for (let i = updates.length - 1; i >= 0; i -= 1) {
187
+ const u = updates[i];
188
+ if (isMessageToolCallUpdate(u) && !done.has(u.uuid)) {
189
+ return u.call.name;
190
+ }
191
+ }
192
+ return null;
193
+ });
194
  let showRouterDetails = $state(false);
195
  let routerDetailsTimeout: ReturnType<typeof setTimeout> | undefined;
196
 
 
259
  let modelIsMultimodalOverride = $derived($settings.multimodalOverrides?.[currentModel.id]);
260
  let modelIsMultimodal = $derived((modelIsMultimodalOverride ?? currentModel.multimodal) === true);
261
 
262
+ // Determine tool support for the current model (server-provided capability with user override)
263
+ let modelSupportsTools = $derived(
264
+ ($settings.toolsOverrides?.[currentModel.id] ??
265
+ (currentModel as unknown as { supportsTools?: boolean }).supportsTools) === true
266
+ );
267
+
268
  // Always allow common text-like files; add images only when model is multimodal
269
  import { TEXT_MIME_ALLOWLIST, IMAGE_MIME_ALLOWLIST_DEFAULT } from "$lib/constants/mime";
270
 
 
526
  {onPaste}
527
  disabled={isReadOnly || lastIsError}
528
  {modelIsMultimodal}
529
+ {modelSupportsTools}
530
  bind:focused
531
  />
532
  {/if}
 
574
  }}
575
  >
576
  {#if models.find((m) => m.id === currentModel.id)}
577
+ {#if loading && streamingToolCallName}
578
+ <span class="inline-flex items-center gap-1 whitespace-nowrap text-xs">
579
+ <CarbonTools class="text-[11px]" />
580
+ Calling tool
581
+ <span class="loading-dots font-medium">
582
+ {availableTools.find((t) => t.name === streamingToolCallName)?.displayName ??
583
+ streamingToolCallName}
584
+ </span>
585
+ </span>
586
+ {:else if !currentModel.isRouter || !loading}
587
  <a
588
  href="{base}/settings/{currentModel.id}"
589
  onclick={(e) => {
 
601
  {/if}
602
  <CarbonCaretDown class="-ml-0.5 text-xxs" />
603
  </a>
604
+ {:else if showRouterDetails && streamingRouterMetadata?.route}
605
  <div
606
  class="mr-2 flex items-center gap-1.5 whitespace-nowrap text-[.70rem] text-xs leading-none text-gray-400 dark:text-gray-400"
607
  >
src/lib/components/chat/FileDropzone.svelte CHANGED
@@ -84,7 +84,7 @@
84
  e.preventDefault();
85
  }}
86
  class="relative flex h-28 w-full max-w-4xl flex-col items-center justify-center gap-1 rounded-xl border-2 border-dotted {onDragInner
87
- ? 'border-blue-200 !bg-blue-500/10 text-blue-600 *:pointer-events-none dark:border-blue-600 dark:bg-blue-500/20 dark:text-blue-500'
88
  : 'bg-gray-100 text-gray-500 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-400'}"
89
  >
90
  <CarbonImage class="text-xl" />
 
84
  e.preventDefault();
85
  }}
86
  class="relative flex h-28 w-full max-w-4xl flex-col items-center justify-center gap-1 rounded-xl border-2 border-dotted {onDragInner
87
+ ? 'border-blue-200 !bg-blue-600/10 text-blue-600 *:pointer-events-none dark:border-blue-600 dark:bg-blue-600/20 dark:text-blue-600'
88
  : 'bg-gray-100 text-gray-500 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-400'}"
89
  >
90
  <CarbonImage class="text-xl" />
src/lib/components/chat/MarkdownRenderer.svelte.test.ts CHANGED
@@ -29,60 +29,6 @@ describe("MarkdownRenderer", () => {
29
  render(MarkdownRenderer, { content: "```foobar```" });
30
  expect(page.getByRole("code")).toHaveTextContent("foobar");
31
  });
32
- it("renders sources correctly", () => {
33
- const props = {
34
- content: "Hello there [1]",
35
- sources: [
36
- {
37
- title: "foo",
38
- link: "https://example.com",
39
- },
40
- ],
41
- };
42
- render(MarkdownRenderer, props);
43
-
44
- const link = page.getByRole("link");
45
- expect(link).toBeInTheDocument();
46
- expect(link).toHaveAttribute("href", "https://example.com");
47
- expect(link).toHaveAttribute("target", "_blank");
48
- expect(link).toHaveAttribute("rel", "noreferrer");
49
- });
50
- it("handles groups of sources", () => {
51
- render(MarkdownRenderer, {
52
- content: "Hello there [1], [2], [3]",
53
- sources: [
54
- {
55
- title: "foo",
56
- link: "https://foo.com",
57
- },
58
- {
59
- title: "bar",
60
- link: "https://bar.com",
61
- },
62
- {
63
- title: "baz",
64
- link: "https://baz.com",
65
- },
66
- ],
67
- });
68
- expect(page.getByRole("link").all()).toHaveLength(3);
69
- expect(page.getByRole("link").nth(0)).toHaveAttribute("href", "https://foo.com");
70
- expect(page.getByRole("link").nth(1)).toHaveAttribute("href", "https://bar.com");
71
- expect(page.getByRole("link").nth(2)).toHaveAttribute("href", "https://baz.com");
72
- });
73
- it("does not render sources in code blocks", () => {
74
- render(MarkdownRenderer, {
75
- content: "```\narray[1]\n```",
76
- sources: [
77
- {
78
- title: "foo",
79
- link: "https://example.com",
80
- },
81
- ],
82
- });
83
- const linkSelector = page.getByRole("link");
84
- expect(linkSelector.elements).toHaveLength(0);
85
- });
86
  it("doesnt render raw html directly", () => {
87
  render(MarkdownRenderer, { content: "<button>Click me</button>" });
88
  expect(page.getByRole("button").elements).toHaveLength(0);
 
29
  render(MarkdownRenderer, { content: "```foobar```" });
30
  expect(page.getByRole("code")).toHaveTextContent("foobar");
31
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  it("doesnt render raw html directly", () => {
33
  render(MarkdownRenderer, { content: "<button>Click me</button>" });
34
  expect(page.getByRole("button").elements).toHaveLength(0);
src/lib/components/chat/OpenReasoningResults.svelte CHANGED
@@ -18,7 +18,7 @@
18
 
19
  <details
20
  bind:open={isOpen}
21
- class="group flex w-fit max-w-full flex-col rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900 [&:has(+_.prose)]:mb-4"
22
  >
23
  <summary
24
  class="
@@ -71,7 +71,7 @@
71
  </summary>
72
 
73
  <div
74
- class="prose prose-sm !max-w-none space-y-4 border-t border-gray-200 p-3 text-sm text-gray-600 dark:prose-invert dark:border-gray-800 dark:text-gray-400"
75
  >
76
  {#key content}
77
  <MarkdownRenderer {content} {loading} />
 
18
 
19
  <details
20
  bind:open={isOpen}
21
+ class="group flex w-fit max-w-full flex-col rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900 [&:has(+_.prose)]:mb-4 [.prose+&]:mt-4 [details+&]:mt-2"
22
  >
23
  <summary
24
  class="
 
71
  </summary>
72
 
73
  <div
74
+ class="prose prose-sm !max-w-none space-y-4 border-t border-gray-200 p-3 text-sm text-gray-600 dark:prose-invert prose-img:my-0 prose-img:rounded-lg dark:border-gray-800 dark:text-gray-400"
75
  >
76
  {#key content}
77
  <MarkdownRenderer {content} {loading} />
src/lib/components/chat/ToolUpdate.svelte ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { MessageToolUpdateType, type MessageToolUpdate } from "$lib/types/MessageUpdate";
3
+ import {
4
+ isMessageToolCallUpdate,
5
+ isMessageToolErrorUpdate,
6
+ isMessageToolResultUpdate,
7
+ } from "$lib/utils/messageUpdates";
8
+ import CarbonTools from "~icons/carbon/tools";
9
+ import { ToolResultStatus, type ToolFront } from "$lib/types/Tool";
10
+ import { page } from "$app/state";
11
+ import { onDestroy } from "svelte";
12
+ import { browser } from "$app/environment";
13
+ import CarbonChevronLeft from "~icons/carbon/chevron-left";
14
+ import CarbonChevronRight from "~icons/carbon/chevron-right";
15
+
16
+ interface Props {
17
+ tool: MessageToolUpdate[];
18
+ loading?: boolean;
19
+ // Optional navigation props when multiple tool groups exist
20
+ index?: number;
21
+ total?: number;
22
+ onprev?: () => void;
23
+ onnext?: () => void;
24
+ }
25
+
26
+ let { tool, loading = false, index, total, onprev, onnext }: Props = $props();
27
+
28
+ let toolFnName = $derived(tool.find(isMessageToolCallUpdate)?.call.name);
29
+ let toolError = $derived(tool.some(isMessageToolErrorUpdate));
30
+ let toolDone = $derived(tool.some(isMessageToolResultUpdate));
31
+ let eta = $derived(tool.find((update) => update.subtype === MessageToolUpdateType.ETA)?.eta);
32
+
33
+ const availableTools: ToolFront[] = $derived.by(
34
+ () => (page.data as { tools?: ToolFront[] } | undefined)?.tools ?? []
35
+ );
36
+
37
+ let loadingBarEl: HTMLDivElement | undefined = $state(undefined);
38
+ let animation: Animation | undefined = $state(undefined);
39
+ let showingLoadingBar = $state(false);
40
+
41
+ const formatValue = (value: unknown): string => {
42
+ if (value == null) return "";
43
+ if (typeof value === "object") {
44
+ try {
45
+ return JSON.stringify(value, null, 2);
46
+ } catch {
47
+ return String(value);
48
+ }
49
+ }
50
+ return String(value);
51
+ };
52
+
53
+ $effect(() => {
54
+ if (!toolError && !toolDone && loading && loadingBarEl && eta) {
55
+ loadingBarEl.classList.remove("hidden");
56
+ showingLoadingBar = true;
57
+ animation = loadingBarEl.animate([{ width: "0%" }, { width: "calc(100%+1rem)" }], {
58
+ duration: (eta ?? 0) * 1000,
59
+ fill: "forwards",
60
+ });
61
+ }
62
+ });
63
+
64
+ onDestroy(() => {
65
+ animation?.cancel();
66
+ });
67
+
68
+ $effect(() => {
69
+ if ((!loading || toolDone || toolError) && browser && loadingBarEl && showingLoadingBar) {
70
+ showingLoadingBar = false;
71
+ loadingBarEl.classList.remove("hidden");
72
+ animation?.cancel();
73
+ const fromWidth = getComputedStyle(loadingBarEl).width;
74
+ animation = loadingBarEl.animate([{ width: fromWidth }, { width: "calc(100%+1rem)" }], {
75
+ duration: 300,
76
+ fill: "forwards",
77
+ });
78
+ setTimeout(() => loadingBarEl?.classList.add("hidden"), 300);
79
+ }
80
+ });
81
+ </script>
82
+
83
+ {#if toolFnName}
84
+ <details
85
+ class="group/tool my-2.5 w-fit max-w-full cursor-pointer rounded-lg border border-gray-200 bg-white px-1 {(total ??
86
+ 0) > 1
87
+ ? ''
88
+ : 'pr-2'} text-sm shadow-sm first:mt-0 open:mb-3 open:border-purple-500/10 open:bg-purple-600/5 open:shadow-sm dark:border-gray-800 dark:bg-gray-900 open:dark:border-purple-800/40 open:dark:bg-purple-800/10 [&+details]:-mt-2"
89
+ >
90
+ <summary
91
+ class="relative flex select-none list-none items-center gap-1.5 py-1 group-open/tool:text-purple-700 group-open/tool:dark:text-purple-300"
92
+ >
93
+ <div
94
+ bind:this={loadingBarEl}
95
+ class="absolute -m-1 hidden h-full w-full rounded-lg bg-purple-500/5 transition-all dark:bg-purple-500/10"
96
+ ></div>
97
+
98
+ <div
99
+ class="relative grid size-[22px] place-items-center rounded bg-purple-600/10 dark:bg-purple-600/20"
100
+ >
101
+ <svg
102
+ class="absolute inset-0 text-purple-500/40 transition-opacity"
103
+ class:invisible={toolDone || toolError}
104
+ width="22"
105
+ height="22"
106
+ viewBox="0 0 38 38"
107
+ fill="none"
108
+ xmlns="http://www.w3.org/2000/svg"
109
+ >
110
+ <path
111
+ class="loading-path"
112
+ d="M8 2.5H30C30 2.5 35.5 2.5 35.5 8V30C35.5 30 35.5 35.5 30 35.5H8C8 35.5 2.5 35.5 2.5 30V8C2.5 8 2.5 2.5 8 2.5Z"
113
+ pathLength="100"
114
+ stroke="currentColor"
115
+ stroke-width="1"
116
+ stroke-linecap="round"
117
+ id="shape"
118
+ />
119
+ </svg>
120
+ <CarbonTools class="text-xs text-purple-700 dark:text-purple-500" />
121
+ </div>
122
+
123
+ <span class="relative">
124
+ {toolError ? "Error calling" : toolDone ? "Called" : "Calling"} tool
125
+ <span class="font-semibold"
126
+ >{availableTools.find((entry) => entry.name === toolFnName)?.displayName ??
127
+ toolFnName}</span
128
+ >
129
+ </span>
130
+
131
+ {#if (total ?? 0) > 1}
132
+ <div class="relative ml-auto flex items-center gap-1.5">
133
+ <div
134
+ class="flex items-center divide-x rounded-md border border-gray-200 bg-gray-50 dark:divide-gray-700 dark:border-gray-800 dark:bg-gray-800"
135
+ >
136
+ <button
137
+ type="button"
138
+ class="btn size-5 text-xs text-gray-500 hover:text-gray-700 focus:ring-0 disabled:opacity-40 dark:text-gray-400 dark:hover:text-gray-200"
139
+ title="Previous tool"
140
+ aria-label="Previous tool"
141
+ disabled={(index ?? 0) <= 0}
142
+ onclick={(e) => {
143
+ e.preventDefault();
144
+ e.stopPropagation();
145
+ onprev?.();
146
+ }}
147
+ >
148
+ <CarbonChevronLeft />
149
+ </button>
150
+
151
+ <span
152
+ class="select-none px-1 text-center text-[10px] font-medium text-gray-500 dark:text-gray-400"
153
+ aria-live="polite"
154
+ >
155
+ {(index ?? 0) + 1} <span class="text-gray-300 dark:text-gray-500">/</span>
156
+ {total}
157
+ </span>
158
+ <button
159
+ type="button"
160
+ class="btn size-5 text-xs text-gray-500 hover:text-gray-700 focus:ring-0 disabled:opacity-40 dark:text-gray-400 dark:hover:text-gray-200"
161
+ title="Next tool"
162
+ aria-label="Next tool"
163
+ disabled={(index ?? 0) >= (total ?? 1) - 1}
164
+ onclick={(e) => {
165
+ e.preventDefault();
166
+ e.stopPropagation();
167
+ onnext?.();
168
+ }}
169
+ >
170
+ <CarbonChevronRight />
171
+ </button>
172
+ </div>
173
+ </div>
174
+ {/if}
175
+ </summary>
176
+
177
+ {#each tool as update}
178
+ {#if update.subtype === MessageToolUpdateType.Call}
179
+ <div class="mt-1 flex items-center gap-2 opacity-80">
180
+ <h3 class="text-sm">Parameters</h3>
181
+ <div class="h-px flex-1 bg-gradient-to-r from-gray-500/20"></div>
182
+ </div>
183
+ <ul class="py-1 text-sm">
184
+ {#each Object.entries(update.call.parameters ?? {}) as [key, value]}
185
+ {#if value != null}
186
+ <li>
187
+ <span class="font-semibold">{key}</span>:
188
+ <span class="whitespace-pre-wrap">{formatValue(value)}</span>
189
+ </li>
190
+ {/if}
191
+ {/each}
192
+ </ul>
193
+ {:else if update.subtype === MessageToolUpdateType.Error}
194
+ <div class="mt-1 flex items-center gap-2 opacity-80">
195
+ <h3 class="text-sm">Error</h3>
196
+ <div class="h-px flex-1 bg-gradient-to-r from-gray-500/20"></div>
197
+ </div>
198
+ <p class="text-sm">{update.message}</p>
199
+ {:else if isMessageToolResultUpdate(update) && update.result.status === ToolResultStatus.Success && update.result.display}
200
+ <div class="mt-1 flex items-center gap-2 opacity-80">
201
+ <h3 class="text-sm">Result</h3>
202
+ <div class="h-px flex-1 bg-gradient-to-r from-gray-500/20"></div>
203
+ </div>
204
+ <ul class="py-1 text-sm">
205
+ {#each update.result.outputs as output}
206
+ {#each Object.entries(output) as [key, value]}
207
+ {#if value != null}
208
+ <li>
209
+ <span class="font-semibold">{key}</span>:
210
+ <span class="whitespace-pre-wrap">{formatValue(value)}</span>
211
+ </li>
212
+ {/if}
213
+ {/each}
214
+ {/each}
215
+ </ul>
216
+ {:else if isMessageToolResultUpdate(update) && update.result.status === ToolResultStatus.Error && update.result.display}
217
+ <div class="mt-1 flex items-center gap-2 opacity-80">
218
+ <h3 class="text-sm text-red-600 dark:text-red-400">Error</h3>
219
+ <div class="h-px flex-1 bg-gradient-to-r from-red-500/20"></div>
220
+ </div>
221
+ <p class="whitespace-pre-wrap text-sm text-red-600 dark:text-red-400">
222
+ {update.result.message}
223
+ </p>
224
+ {/if}
225
+ {/each}
226
+ </details>
227
+ {/if}
228
+
229
+ <style>
230
+ details summary::-webkit-details-marker {
231
+ display: none;
232
+ }
233
+
234
+ @keyframes loading {
235
+ to {
236
+ /* move one full perimeter, normalized via pathLength=100 */
237
+ stroke-dashoffset: -100;
238
+ }
239
+ }
240
+
241
+ .loading-path {
242
+ /* larger traveling gap for clearer motion */
243
+ stroke-dasharray: 80 20; /* 80% dash, 20% gap */
244
+ animation: loading 1.6s linear infinite;
245
+ }
246
+ </style>
src/lib/components/icons/IconMCP.svelte ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ interface Props {
3
+ classNames?: string;
4
+ }
5
+
6
+ let { classNames = "" }: Props = $props();
7
+ </script>
8
+
9
+ <svg
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ class={classNames}
12
+ width="1em"
13
+ height="1em"
14
+ viewBox="0 0 24 24"
15
+ >
16
+ <g
17
+ fill="none"
18
+ stroke="currentColor"
19
+ stroke-linecap="round"
20
+ stroke-linejoin="round"
21
+ stroke-width="1.5"
22
+ >
23
+ <path
24
+ d="m3.5 11.75l8.172-8.171a2.828 2.828 0 1 1 4 4m0 0L9.5 13.75m6.172-6.171a2.828 2.828 0 0 1 4 4l-6.965 6.964a1 1 0 0 0 0 1.414L14 21.25"
25
+ />
26
+ <path d="m17.5 9.75l-6.172 6.171a2.829 2.829 0 0 1-4-4L13.5 5.749" />
27
+ </g>
28
+ </svg>
src/lib/components/mcp/AddServerForm.svelte ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { KeyValuePair } from "$lib/types/Tool";
3
+ import {
4
+ validateMcpServerUrl,
5
+ validateHeader,
6
+ isSensitiveHeader,
7
+ } from "$lib/utils/mcpValidation";
8
+ import IconEye from "~icons/carbon/view";
9
+ import IconEyeOff from "~icons/carbon/view-off";
10
+ import IconTrash from "~icons/carbon/trash-can";
11
+ import IconAdd from "~icons/carbon/add";
12
+ import IconWarning from "~icons/carbon/warning";
13
+
14
+ interface Props {
15
+ onsubmit: (server: { name: string; url: string; headers?: KeyValuePair[] }) => void;
16
+ oncancel: () => void;
17
+ initialName?: string;
18
+ initialUrl?: string;
19
+ initialHeaders?: KeyValuePair[];
20
+ submitLabel?: string;
21
+ }
22
+
23
+ let {
24
+ onsubmit,
25
+ oncancel,
26
+ initialName = "",
27
+ initialUrl = "",
28
+ initialHeaders = [],
29
+ submitLabel = "Add Server",
30
+ }: Props = $props();
31
+
32
+ let name = $state(initialName);
33
+ let url = $state(initialUrl);
34
+ let headers = $state<KeyValuePair[]>(initialHeaders.length > 0 ? [...initialHeaders] : []);
35
+ let showHeaderValues = $state<Record<number, boolean>>({});
36
+ let error = $state<string | null>(null);
37
+
38
+ function addHeader() {
39
+ headers = [...headers, { key: "", value: "" }];
40
+ }
41
+
42
+ function removeHeader(index: number) {
43
+ headers = headers.filter((_, i) => i !== index);
44
+ delete showHeaderValues[index];
45
+ }
46
+
47
+ function toggleHeaderVisibility(index: number) {
48
+ showHeaderValues = {
49
+ ...showHeaderValues,
50
+ [index]: !showHeaderValues[index],
51
+ };
52
+ }
53
+
54
+ function validate(): boolean {
55
+ if (!name.trim()) {
56
+ error = "Server name is required";
57
+ return false;
58
+ }
59
+
60
+ if (!url.trim()) {
61
+ error = "Server URL is required";
62
+ return false;
63
+ }
64
+
65
+ const urlValidation = validateMcpServerUrl(url);
66
+ if (!urlValidation) {
67
+ error = "Invalid URL.";
68
+ return false;
69
+ }
70
+
71
+ // Validate headers
72
+ for (let i = 0; i < headers.length; i++) {
73
+ const header = headers[i];
74
+ if (header.key.trim() || header.value.trim()) {
75
+ const headerError = validateHeader(header.key, header.value);
76
+ if (headerError) {
77
+ error = `Header ${i + 1}: ${headerError}`;
78
+ return false;
79
+ }
80
+ }
81
+ }
82
+
83
+ error = null;
84
+ return true;
85
+ }
86
+
87
+ function handleSubmit() {
88
+ if (!validate()) return;
89
+
90
+ // Filter out empty headers
91
+ const filteredHeaders = headers.filter((h) => h.key.trim() && h.value.trim());
92
+
93
+ onsubmit({
94
+ name: name.trim(),
95
+ url: url.trim(),
96
+ headers: filteredHeaders.length > 0 ? filteredHeaders : undefined,
97
+ });
98
+ }
99
+ </script>
100
+
101
+ <div class="space-y-4">
102
+ <!-- Server Name -->
103
+ <div>
104
+ <label
105
+ for="server-name"
106
+ class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
107
+ >
108
+ Server Name <span class="text-red-500">*</span>
109
+ </label>
110
+ <input
111
+ id="server-name"
112
+ type="text"
113
+ bind:value={name}
114
+ placeholder="My MCP Server"
115
+ class="mt-1.5 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white"
116
+ />
117
+ </div>
118
+
119
+ <!-- Server URL -->
120
+ <div>
121
+ <label for="server-url" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
122
+ Server URL <span class="text-red-500">*</span>
123
+ </label>
124
+ <input
125
+ id="server-url"
126
+ type="url"
127
+ bind:value={url}
128
+ placeholder="https://example.com/mcp"
129
+ class="mt-1.5 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white"
130
+ />
131
+ <!-- <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
132
+ Only HTTPS is supported (e.g., https://localhost:5101).
133
+ </p> -->
134
+ </div>
135
+
136
+ <!-- HTTP Headers -->
137
+ <details class="rounded-lg border border-gray-200 dark:border-gray-700">
138
+ <summary class="cursor-pointer px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300">
139
+ HTTP Headers (Optional)
140
+ </summary>
141
+ <div class="space-y-2 border-t border-gray-200 p-4 dark:border-gray-700">
142
+ {#if headers.length === 0}
143
+ <p class="text-sm text-gray-500 dark:text-gray-400">No headers configured</p>
144
+ {:else}
145
+ {#each headers as header, i}
146
+ <div class="flex gap-2">
147
+ <input
148
+ bind:value={header.key}
149
+ placeholder="Header name (e.g., Authorization)"
150
+ class="flex-1 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white"
151
+ />
152
+ <div class="relative flex-1">
153
+ <input
154
+ bind:value={header.value}
155
+ type={showHeaderValues[i] ? "text" : "password"}
156
+ placeholder="Value"
157
+ class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 pr-10 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white"
158
+ />
159
+ {#if isSensitiveHeader(header.key)}
160
+ <button
161
+ type="button"
162
+ onclick={() => toggleHeaderVisibility(i)}
163
+ class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
164
+ title={showHeaderValues[i] ? "Hide value" : "Show value"}
165
+ >
166
+ {#if showHeaderValues[i]}
167
+ <IconEyeOff class="size-4" />
168
+ {:else}
169
+ <IconEye class="size-4" />
170
+ {/if}
171
+ </button>
172
+ {/if}
173
+ </div>
174
+ <button
175
+ type="button"
176
+ onclick={() => removeHeader(i)}
177
+ class="rounded-lg bg-red-100 p-2 text-red-600 hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50"
178
+ title="Remove header"
179
+ >
180
+ <IconTrash class="size-4" />
181
+ </button>
182
+ </div>
183
+ {/each}
184
+ {/if}
185
+
186
+ <button
187
+ type="button"
188
+ onclick={addHeader}
189
+ class="flex items-center gap-1.5 rounded-lg bg-gray-100 px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
190
+ >
191
+ <IconAdd class="size-4" />
192
+ Add Header
193
+ </button>
194
+
195
+ <p class="text-xs text-gray-500 dark:text-gray-400">
196
+ Common examples:<br />
197
+ • Bearer token:
198
+ <code class="rounded bg-gray-100 px-1 dark:bg-gray-700"
199
+ >Authorization: Bearer YOUR_TOKEN</code
200
+ ><br />
201
+ • API key:
202
+ <code class="rounded bg-gray-100 px-1 dark:bg-gray-700">X-API-Key: YOUR_KEY</code>
203
+ </p>
204
+ </div>
205
+ </details>
206
+
207
+ <!-- Security warning about custom MCP servers -->
208
+ <div
209
+ class="rounded-lg border border-amber-200 bg-amber-50 p-3 text-amber-900 dark:border-yellow-900/40 dark:bg-yellow-900/20 dark:text-yellow-100"
210
+ >
211
+ <div class="flex items-start gap-3">
212
+ <IconWarning class="mt-0.5 size-4 flex-none text-amber-600 dark:text-yellow-300" />
213
+ <div class="text-sm leading-5">
214
+ <p class="font-medium">Be careful with custom MCP servers.</p>
215
+ <p class="mt-1 text-[13px] text-amber-800 dark:text-yellow-100/90">
216
+ They receive your requests (including conversation context and any headers you add) and
217
+ can run powerful tools on your behalf. Only add servers you trust and review their source.
218
+ Never share confidental informations.
219
+ </p>
220
+ </div>
221
+ </div>
222
+ </div>
223
+
224
+ <!-- Error message -->
225
+ {#if error}
226
+ <div
227
+ class="rounded-lg border border-red-200 bg-red-50 p-3 dark:border-red-800 dark:bg-red-900/20"
228
+ >
229
+ <p class="text-sm text-red-800 dark:text-red-200">{error}</p>
230
+ </div>
231
+ {/if}
232
+
233
+ <!-- Actions -->
234
+ <div class="flex justify-end gap-2">
235
+ <button
236
+ type="button"
237
+ onclick={oncancel}
238
+ class="rounded-lg bg-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
239
+ >
240
+ Cancel
241
+ </button>
242
+ <button
243
+ type="button"
244
+ onclick={handleSubmit}
245
+ class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-600"
246
+ >
247
+ {submitLabel}
248
+ </button>
249
+ </div>
250
+ </div>
src/lib/components/mcp/MCPServerManager.svelte ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
3
+ import Modal from "$lib/components/Modal.svelte";
4
+ import ServerCard from "./ServerCard.svelte";
5
+ import AddServerForm from "./AddServerForm.svelte";
6
+ import {
7
+ allMcpServers,
8
+ selectedServerIds,
9
+ enabledServersCount,
10
+ addCustomServer,
11
+ refreshMcpServers,
12
+ healthCheckServer,
13
+ } from "$lib/stores/mcpServers";
14
+ import type { KeyValuePair } from "$lib/types/Tool";
15
+ import IconAddLarge from "~icons/carbon/add-large";
16
+ import IconRefresh from "~icons/carbon/renew";
17
+ import IconTools from "~icons/carbon/tools";
18
+ import IconMCP from "$lib/components/icons/IconMCP.svelte";
19
+
20
+ const publicConfig = usePublicConfig();
21
+
22
+ interface Props {
23
+ onclose: () => void;
24
+ }
25
+
26
+ let { onclose }: Props = $props();
27
+
28
+ type View = "list" | "add";
29
+ let currentView = $state<View>("list");
30
+ let isRefreshing = $state(false);
31
+
32
+ const baseServers = $derived($allMcpServers.filter((s) => s.type === "base"));
33
+ const customServers = $derived($allMcpServers.filter((s) => s.type === "custom"));
34
+ const enabledCount = $derived($enabledServersCount);
35
+
36
+ function handleAddServer(serverData: { name: string; url: string; headers?: KeyValuePair[] }) {
37
+ addCustomServer(serverData);
38
+ currentView = "list";
39
+ }
40
+
41
+ function handleCancel() {
42
+ currentView = "list";
43
+ }
44
+
45
+ async function handleRefresh() {
46
+ if (isRefreshing) return;
47
+ isRefreshing = true;
48
+ try {
49
+ await refreshMcpServers();
50
+ // After refreshing the list, re-run health checks for all known servers
51
+ const servers = $allMcpServers;
52
+ await Promise.allSettled(servers.map((s) => healthCheckServer(s)));
53
+ } finally {
54
+ isRefreshing = false;
55
+ }
56
+ }
57
+ </script>
58
+
59
+ <Modal width={currentView === "list" ? "w-[800px]" : "w-[600px]"} {onclose} closeButton>
60
+ <div class="p-6">
61
+ <!-- Header -->
62
+ <div class="mb-6">
63
+ <h2 class="mb-1 text-xl font-semibold text-gray-900 dark:text-gray-200">
64
+ {#if currentView === "list"}
65
+ MCP Servers
66
+ {:else}
67
+ Add MCP server
68
+ {/if}
69
+ </h2>
70
+ <p class="text-sm text-gray-600 dark:text-gray-400">
71
+ {#if currentView === "list"}
72
+ Manage MCP servers to extend {publicConfig.PUBLIC_APP_NAME} with external tools.
73
+ {:else}
74
+ Add a custom MCP server to {publicConfig.PUBLIC_APP_NAME}.
75
+ {/if}
76
+ </p>
77
+ </div>
78
+
79
+ <!-- Content -->
80
+ {#if currentView === "list"}
81
+ <div
82
+ class="mb-6 flex justify-between rounded-lg p-4 max-sm:flex-col max-sm:gap-4 sm:items-center {!enabledCount
83
+ ? 'bg-gray-100 dark:bg-white/5'
84
+ : 'bg-blue-50 dark:bg-blue-900/10'}"
85
+ >
86
+ <div class="flex items-center gap-3">
87
+ <div
88
+ class="flex size-10 items-center justify-center rounded-xl bg-blue-500/10"
89
+ class:grayscale={!enabledCount}
90
+ >
91
+ <IconMCP classNames="size-8 text-blue-600 dark:text-blue-500" />
92
+ </div>
93
+ <div>
94
+ <p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
95
+ {$allMcpServers.length}
96
+ {$allMcpServers.length === 1 ? "server" : "servers"} configured
97
+ </p>
98
+ <p class="text-xs text-gray-600 dark:text-gray-400">
99
+ {enabledCount} enabled
100
+ </p>
101
+ </div>
102
+ </div>
103
+
104
+ <div class="flex gap-2">
105
+ <button
106
+ onclick={handleRefresh}
107
+ disabled={isRefreshing}
108
+ class="btn gap-1.5 rounded-lg border border-gray-200 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
109
+ >
110
+ <IconRefresh class="size-4 {isRefreshing ? 'animate-spin' : ''}" />
111
+ {isRefreshing ? "Refreshing…" : "Refresh"}
112
+ </button>
113
+ <button
114
+ onclick={() => (currentView = "add")}
115
+ class="btn flex items-center gap-0.5 rounded-lg bg-blue-600 py-1.5 pl-2 pr-3 text-sm font-medium text-white hover:bg-blue-600"
116
+ >
117
+ <IconAddLarge class="size-4" />
118
+ Add Server
119
+ </button>
120
+ </div>
121
+ </div>
122
+ <div class="space-y-5">
123
+ <!-- Base Servers -->
124
+ {#if baseServers.length > 0}
125
+ <div>
126
+ <h3 class="mb-3 text-sm font-medium text-gray-700 dark:text-gray-300">
127
+ Base Servers ({baseServers.length})
128
+ </h3>
129
+ <div class="grid grid-cols-1 gap-3 md:grid-cols-2">
130
+ {#each baseServers as server (server.id)}
131
+ <ServerCard {server} isSelected={$selectedServerIds.has(server.id)} />
132
+ {/each}
133
+ </div>
134
+ </div>
135
+ {/if}
136
+
137
+ <!-- Custom Servers -->
138
+ <div>
139
+ <h3 class="mb-3 text-sm font-medium text-gray-700 dark:text-gray-300">
140
+ Custom Servers ({customServers.length})
141
+ </h3>
142
+ {#if customServers.length === 0}
143
+ <div
144
+ class="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 p-8 dark:border-gray-700"
145
+ >
146
+ <IconTools class="mb-3 size-12 text-gray-400" />
147
+ <p class="mb-1 text-sm font-medium text-gray-900 dark:text-gray-100">
148
+ No custom servers yet
149
+ </p>
150
+ <p class="mb-4 text-xs text-gray-600 dark:text-gray-400">
151
+ Add your own MCP servers with custom tools
152
+ </p>
153
+ <button
154
+ onclick={() => (currentView = "add")}
155
+ class="flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-600"
156
+ >
157
+ <IconAddLarge class="size-4" />
158
+ Add Your First Server
159
+ </button>
160
+ </div>
161
+ {:else}
162
+ <div class="grid grid-cols-1 gap-3 md:grid-cols-2">
163
+ {#each customServers as server (server.id)}
164
+ <ServerCard {server} isSelected={$selectedServerIds.has(server.id)} />
165
+ {/each}
166
+ </div>
167
+ {/if}
168
+ </div>
169
+
170
+ <!-- Help Text -->
171
+ <div class="rounded-lg bg-gray-50 p-4 dark:bg-gray-700">
172
+ <h4 class="mb-2 text-sm font-medium text-gray-900 dark:text-gray-100">💡 Quick Tips</h4>
173
+ <ul class="space-y-1 text-xs text-gray-600 dark:text-gray-400">
174
+ <li>• Only connect to servers you trust</li>
175
+ <li>• Enable servers to make their tools available in chat</li>
176
+ <li>• Use the Health Check button to verify server connectivity</li>
177
+ <li>• You can add HTTP headers for authentication when required</li>
178
+ </ul>
179
+ </div>
180
+ </div>
181
+ {:else if currentView === "add"}
182
+ <AddServerForm onsubmit={handleAddServer} oncancel={handleCancel} />
183
+ {/if}
184
+ </div>
185
+ </Modal>
src/lib/components/mcp/ServerCard.svelte ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { MCPServer } from "$lib/types/Tool";
3
+ import { toggleServer, healthCheckServer, deleteCustomServer } from "$lib/stores/mcpServers";
4
+ import IconCheckmark from "~icons/carbon/checkmark-filled";
5
+ import IconWarning from "~icons/carbon/warning-filled";
6
+ import IconPending from "~icons/carbon/pending-filled";
7
+ import IconRefresh from "~icons/carbon/renew";
8
+ import IconTrash from "~icons/carbon/trash-can";
9
+ import IconTools from "~icons/carbon/tools";
10
+ import IconSettings from "~icons/carbon/settings";
11
+ import Switch from "$lib/components/Switch.svelte";
12
+ import { getMcpServerFaviconUrl } from "$lib/utils/favicon";
13
+
14
+ interface Props {
15
+ server: MCPServer;
16
+ isSelected: boolean;
17
+ }
18
+
19
+ let { server, isSelected }: Props = $props();
20
+
21
+ let isLoadingHealth = $state(false);
22
+
23
+ // Show a quick-access link ONLY for the exact HF MCP login endpoint
24
+ import { isStrictHfMcpLogin as isStrictHfMcpLoginUrl } from "$lib/utils/hf";
25
+ const isHfMcp = $derived.by(() => isStrictHfMcpLoginUrl(server.url));
26
+
27
+ const statusInfo = $derived.by(() => {
28
+ switch (server.status) {
29
+ case "connected":
30
+ return {
31
+ label: "Connected",
32
+ color: "text-green-600 dark:text-green-400",
33
+ bgColor: "bg-green-100 dark:bg-green-900/20",
34
+ icon: IconCheckmark,
35
+ };
36
+ case "connecting":
37
+ return {
38
+ label: "Connecting...",
39
+ color: "text-blue-600 dark:text-blue-400",
40
+ bgColor: "bg-blue-100 dark:bg-blue-900/20",
41
+ icon: IconPending,
42
+ };
43
+ case "error":
44
+ return {
45
+ label: "Error",
46
+ color: "text-red-600 dark:text-red-400",
47
+ bgColor: "bg-red-100 dark:bg-red-900/20",
48
+ icon: IconWarning,
49
+ };
50
+ case "disconnected":
51
+ default:
52
+ return {
53
+ label: "Unknown",
54
+ color: "text-gray-600 dark:text-gray-400",
55
+ bgColor: "bg-gray-100 dark:bg-gray-700",
56
+ icon: IconPending,
57
+ };
58
+ }
59
+ });
60
+
61
+ // Switch setter handles enable/disable (simple, idiomatic)
62
+ function setEnabled(v: boolean) {
63
+ if (v === isSelected) return;
64
+ toggleServer(server.id);
65
+ if (v && server.status !== "connected") handleHealthCheck();
66
+ }
67
+
68
+ async function handleHealthCheck() {
69
+ isLoadingHealth = true;
70
+ try {
71
+ await healthCheckServer(server);
72
+ } finally {
73
+ isLoadingHealth = false;
74
+ }
75
+ }
76
+
77
+ function handleDelete() {
78
+ deleteCustomServer(server.id);
79
+ }
80
+ </script>
81
+
82
+ <div
83
+ class="rounded-lg border bg-gradient-to-br transition-colors {isSelected
84
+ ? 'border-blue-600/20 bg-blue-50 from-blue-500/5 to-transparent dark:border-blue-700/60 dark:bg-blue-900/10 dark:from-blue-900/20'
85
+ : 'border-gray-200 bg-white from-black/5 dark:border-gray-700 dark:bg-gray-800 dark:from-white/5'}"
86
+ >
87
+ <div class="px-4 py-3.5">
88
+ <!-- Header -->
89
+ <div class="mb-3 flex items-start justify-between gap-3">
90
+ <div class="min-w-0 flex-1">
91
+ <div class="mb-0.5 flex items-center gap-2">
92
+ <img
93
+ src={getMcpServerFaviconUrl(server.url)}
94
+ alt=""
95
+ class="size-4 flex-shrink-0 rounded"
96
+ />
97
+ <h3 class="truncate font-semibold text-gray-900 dark:text-gray-100">
98
+ {server.name}
99
+ </h3>
100
+ </div>
101
+ <p class="truncate text-sm text-gray-600 dark:text-gray-400">
102
+ {server.url}
103
+ </p>
104
+ </div>
105
+
106
+ <!-- Enable Switch (function binding per Svelte 5 docs) -->
107
+ <Switch name={`enable-${server.id}`} bind:checked={() => isSelected, setEnabled} />
108
+ </div>
109
+
110
+ <!-- Status -->
111
+ {#if server.status}
112
+ <div class="mb-2 flex items-center gap-2">
113
+ <span
114
+ class="inline-flex items-center gap-1 rounded-full {statusInfo.bgColor} py-0.5 pl-1.5 pr-2 text-xs font-medium {statusInfo.color}"
115
+ >
116
+ {#if server.status === "connected"}
117
+ <IconCheckmark class="size-3" />
118
+ {:else if server.status === "connecting"}
119
+ <IconPending class="size-3" />
120
+ {:else if server.status === "error"}
121
+ <IconWarning class="size-3" />
122
+ {:else}
123
+ <IconPending class="size-3" />
124
+ {/if}
125
+ {statusInfo.label}
126
+ </span>
127
+
128
+ {#if server.tools && server.tools.length > 0}
129
+ <span class="inline-flex items-center gap-1 text-xs text-gray-600 dark:text-gray-400">
130
+ <IconTools class="size-3" />
131
+ {server.tools.length}
132
+ {server.tools.length === 1 ? "tool" : "tools"}
133
+ </span>
134
+ {/if}
135
+ </div>
136
+ {/if}
137
+
138
+ <!-- Error Message -->
139
+ {#if server.errorMessage}
140
+ <div class="mb-2 flex items-center gap-2">
141
+ <div
142
+ class="rounded bg-red-50 px-2 py-1 text-xs text-red-800 dark:bg-red-900/20 dark:text-red-200"
143
+ >
144
+ {server.errorMessage}
145
+ </div>
146
+ </div>
147
+ {/if}
148
+
149
+ <!-- Actions -->
150
+ <div class="flex flex-wrap gap-1">
151
+ <button
152
+ onclick={handleHealthCheck}
153
+ disabled={isLoadingHealth}
154
+ class="flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-2.5 py-[.29rem] text-xs font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
155
+ >
156
+ <IconRefresh class="size-3 {isLoadingHealth ? 'animate-spin' : ''}" />
157
+ Health Check
158
+ </button>
159
+
160
+ {#if isHfMcp}
161
+ <a
162
+ href="https://huggingface.co/settings/mcp"
163
+ target="_blank"
164
+ rel="noopener noreferrer"
165
+ class="flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-2.5 py-[.29rem] text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
166
+ aria-label="Open Hugging Face MCP settings"
167
+ >
168
+ <IconSettings class="size-3" />
169
+ Settings
170
+ </a>
171
+ {/if}
172
+
173
+ {#if server.type === "custom"}
174
+ <button
175
+ onclick={handleDelete}
176
+ class="flex items-center gap-1.5 rounded-lg border border-red-500/15 bg-red-50 px-2.5 py-[.29rem] text-xs font-medium text-red-600 hover:bg-red-100 dark:border-red-500/25 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50"
177
+ >
178
+ <IconTrash class="size-3" />
179
+ Delete
180
+ </button>
181
+ {/if}
182
+ </div>
183
+
184
+ <!-- Tools List (Expandable) -->
185
+ {#if server.tools && server.tools.length > 0}
186
+ <details class="mt-3">
187
+ <summary class="cursor-pointer text-xs font-medium text-gray-700 dark:text-gray-300">
188
+ Available Tools ({server.tools.length})
189
+ </summary>
190
+ <ul class="mt-2 space-y-1 text-xs">
191
+ {#each server.tools as tool}
192
+ <li class="text-gray-600 dark:text-gray-400">
193
+ <span class="font-medium text-gray-900 dark:text-gray-100">{tool.name}</span>
194
+ {#if tool.description}
195
+ <span class="text-gray-500 dark:text-gray-500">- {tool.description}</span>
196
+ {/if}
197
+ </li>
198
+ {/each}
199
+ </ul>
200
+ </details>
201
+ {/if}
202
+ </div>
203
+ </div>
src/lib/server/api/routes/groups/models.ts CHANGED
@@ -21,6 +21,7 @@ export type GETModelsResponse = Array<{
21
  preprompt?: string;
22
  multimodal: boolean;
23
  multimodalAcceptedMimetypes?: string[];
 
24
  unlisted: boolean;
25
  hasInferenceAPI: boolean;
26
  // Mark router entry for UI decoration — always present
@@ -59,6 +60,7 @@ export const modelGroup = new Elysia().group("/models", (app) =>
59
  preprompt: model.preprompt,
60
  multimodal: model.multimodal,
61
  multimodalAcceptedMimetypes: model.multimodalAcceptedMimetypes,
 
62
  unlisted: model.unlisted,
63
  hasInferenceAPI: model.hasInferenceAPI,
64
  isRouter: model.isRouter,
 
21
  preprompt?: string;
22
  multimodal: boolean;
23
  multimodalAcceptedMimetypes?: string[];
24
+ supportsTools?: boolean;
25
  unlisted: boolean;
26
  hasInferenceAPI: boolean;
27
  // Mark router entry for UI decoration — always present
 
60
  preprompt: model.preprompt,
61
  multimodal: model.multimodal,
62
  multimodalAcceptedMimetypes: model.multimodalAcceptedMimetypes,
63
+ supportsTools: (model as unknown as { supportsTools?: boolean }).supportsTools ?? false,
64
  unlisted: model.unlisted,
65
  hasInferenceAPI: model.hasInferenceAPI,
66
  isRouter: model.isRouter,
src/lib/server/api/routes/groups/user.ts CHANGED
@@ -71,6 +71,7 @@ export const userGroup = new Elysia()
71
 
72
  customPrompts: settings?.customPrompts ?? {},
73
  multimodalOverrides: settings?.multimodalOverrides ?? {},
 
74
  };
75
  })
76
  .post("/settings", async ({ locals, request }) => {
@@ -85,14 +86,13 @@ export const userGroup = new Elysia()
85
  activeModel: z.string().default(DEFAULT_SETTINGS.activeModel),
86
  customPrompts: z.record(z.string()).default({}),
87
  multimodalOverrides: z.record(z.boolean()).default({}),
 
88
  disableStream: z.boolean().default(false),
89
  directPaste: z.boolean().default(false),
90
  hidePromptExamples: z.record(z.boolean()).default({}),
91
  })
92
  .parse(body) satisfies SettingsEditable;
93
 
94
- // Tools removed: ignore tools updates
95
-
96
  await collections.settings.updateOne(
97
  authCondition(locals),
98
  {
 
71
 
72
  customPrompts: settings?.customPrompts ?? {},
73
  multimodalOverrides: settings?.multimodalOverrides ?? {},
74
+ toolsOverrides: settings?.toolsOverrides ?? {},
75
  };
76
  })
77
  .post("/settings", async ({ locals, request }) => {
 
86
  activeModel: z.string().default(DEFAULT_SETTINGS.activeModel),
87
  customPrompts: z.record(z.string()).default({}),
88
  multimodalOverrides: z.record(z.boolean()).default({}),
89
+ toolsOverrides: z.record(z.boolean()).default({}),
90
  disableStream: z.boolean().default(false),
91
  directPaste: z.boolean().default(false),
92
  hidePromptExamples: z.record(z.boolean()).default({}),
93
  })
94
  .parse(body) satisfies SettingsEditable;
95
 
 
 
96
  await collections.settings.updateOne(
97
  authCondition(locals),
98
  {
src/lib/server/config.ts CHANGED
@@ -156,7 +156,9 @@ type ExtraConfigKeys =
156
  | "OLD_MODELS"
157
  | "ENABLE_ASSISTANTS"
158
  | "METRICS_ENABLED"
159
- | "METRICS_PORT";
 
 
160
 
161
  type ConfigProxy = ConfigManager & { [K in ConfigKey | ExtraConfigKeys]: string };
162
 
 
156
  | "OLD_MODELS"
157
  | "ENABLE_ASSISTANTS"
158
  | "METRICS_ENABLED"
159
+ | "METRICS_PORT"
160
+ | "MCP_SERVERS"
161
+ | "MCP_FORWARD_HF_USER_TOKEN";
162
 
163
  type ConfigProxy = ConfigManager & { [K in ConfigKey | ExtraConfigKeys]: string };
164
 
src/lib/server/endpoints/openai/endpointOai.ts CHANGED
@@ -171,12 +171,10 @@ export async function endpointOai(
171
  await prepareMessages(messages, imageProcessor, isMultimodal ?? model.multimodal);
172
 
173
  // Normalize preprompt and handle empty values
174
- const normalizedPreprompt =
175
- typeof preprompt === "string" ? preprompt.trim() : "";
176
 
177
- // Check if a system message already exists as the first message
178
- const hasSystemMessage =
179
- messagesOpenAI.length > 0 && messagesOpenAI[0]?.role === "system";
180
 
181
  if (hasSystemMessage) {
182
  // Prepend normalized preprompt to existing system content when non-empty
@@ -188,15 +186,12 @@ export async function endpointOai(
188
  messagesOpenAI[0].content =
189
  normalizedPreprompt + (userSystemPrompt ? "\n\n" + userSystemPrompt : "");
190
  }
191
- } else {
192
- // Insert a system message only if the preprompt is non-empty
193
- if (normalizedPreprompt) {
194
- messagesOpenAI = [
195
- { role: "system", content: normalizedPreprompt },
196
- ...messagesOpenAI,
197
- ];
198
- }
199
  }
 
200
 
201
  // Combine model defaults with request-specific parameters
202
  const parameters = { ...model.parameters, ...generateSettings };
 
171
  await prepareMessages(messages, imageProcessor, isMultimodal ?? model.multimodal);
172
 
173
  // Normalize preprompt and handle empty values
174
+ const normalizedPreprompt = typeof preprompt === "string" ? preprompt.trim() : "";
 
175
 
176
+ // Check if a system message already exists as the first message
177
+ const hasSystemMessage = messagesOpenAI.length > 0 && messagesOpenAI[0]?.role === "system";
 
178
 
179
  if (hasSystemMessage) {
180
  // Prepend normalized preprompt to existing system content when non-empty
 
186
  messagesOpenAI[0].content =
187
  normalizedPreprompt + (userSystemPrompt ? "\n\n" + userSystemPrompt : "");
188
  }
189
+ } else {
190
+ // Insert a system message only if the preprompt is non-empty
191
+ if (normalizedPreprompt) {
192
+ messagesOpenAI = [{ role: "system", content: normalizedPreprompt }, ...messagesOpenAI];
 
 
 
 
193
  }
194
+ }
195
 
196
  // Combine model defaults with request-specific parameters
197
  const parameters = { ...model.parameters, ...generateSettings };
src/lib/server/endpoints/preprocessMessages.ts CHANGED
@@ -4,13 +4,13 @@ import { downloadFile } from "../files/downloadFile";
4
  import type { ObjectId } from "mongodb";
5
 
6
  export async function preprocessMessages(
7
- messages: Message[],
8
- convId: ObjectId
9
  ): Promise<EndpointMessage[]> {
10
- return Promise.resolve(messages)
11
- .then((msgs) => downloadFiles(msgs, convId))
12
- .then((msgs) => injectClipboardFiles(msgs))
13
- .then(stripEmptyInitialSystemMessage);
14
  }
15
 
16
  async function downloadFiles(messages: Message[], convId: ObjectId): Promise<EndpointMessage[]> {
@@ -24,8 +24,8 @@ async function downloadFiles(messages: Message[], convId: ObjectId): Promise<End
24
  }
25
 
26
  async function injectClipboardFiles(messages: EndpointMessage[]) {
27
- return Promise.all(
28
- messages.map((message) => {
29
  const plaintextFiles = message.files
30
  ?.filter((file) => file.mime === "application/vnd.chatui.clipboard")
31
  .map((file) => Buffer.from(file.value, "base64").toString("utf-8"));
@@ -37,8 +37,8 @@ async function injectClipboardFiles(messages: EndpointMessage[]) {
37
  content: `${plaintextFiles.join("\n\n")}\n\n${message.content}`,
38
  files: message.files?.filter((file) => file.mime !== "application/vnd.chatui.clipboard"),
39
  };
40
- })
41
- );
42
  }
43
 
44
  /**
@@ -46,17 +46,16 @@ async function injectClipboardFiles(messages: EndpointMessage[]) {
46
  * This prevents sending an empty system prompt to any provider.
47
  */
48
  function stripEmptyInitialSystemMessage(messages: EndpointMessage[]): EndpointMessage[] {
49
- if (!messages?.length) return messages;
50
- const first = messages[0];
51
- if (first?.from !== "system") return messages;
52
 
53
- const content = first?.content as unknown;
54
- const isEmpty =
55
- typeof content === "string" ? content.trim().length === 0 : false;
56
 
57
- if (isEmpty) {
58
- return messages.slice(1);
59
- }
60
 
61
- return messages;
62
  }
 
4
  import type { ObjectId } from "mongodb";
5
 
6
  export async function preprocessMessages(
7
+ messages: Message[],
8
+ convId: ObjectId
9
  ): Promise<EndpointMessage[]> {
10
+ return Promise.resolve(messages)
11
+ .then((msgs) => downloadFiles(msgs, convId))
12
+ .then((msgs) => injectClipboardFiles(msgs))
13
+ .then(stripEmptyInitialSystemMessage);
14
  }
15
 
16
  async function downloadFiles(messages: Message[], convId: ObjectId): Promise<EndpointMessage[]> {
 
24
  }
25
 
26
  async function injectClipboardFiles(messages: EndpointMessage[]) {
27
+ return Promise.all(
28
+ messages.map((message) => {
29
  const plaintextFiles = message.files
30
  ?.filter((file) => file.mime === "application/vnd.chatui.clipboard")
31
  .map((file) => Buffer.from(file.value, "base64").toString("utf-8"));
 
37
  content: `${plaintextFiles.join("\n\n")}\n\n${message.content}`,
38
  files: message.files?.filter((file) => file.mime !== "application/vnd.chatui.clipboard"),
39
  };
40
+ })
41
+ );
42
  }
43
 
44
  /**
 
46
  * This prevents sending an empty system prompt to any provider.
47
  */
48
  function stripEmptyInitialSystemMessage(messages: EndpointMessage[]): EndpointMessage[] {
49
+ if (!messages?.length) return messages;
50
+ const first = messages[0];
51
+ if (first?.from !== "system") return messages;
52
 
53
+ const content = first?.content as unknown;
54
+ const isEmpty = typeof content === "string" ? content.trim().length === 0 : false;
 
55
 
56
+ if (isEmpty) {
57
+ return messages.slice(1);
58
+ }
59
 
60
+ return messages;
61
  }
src/lib/server/mcp/clientPool.ts ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Client } from "@modelcontextprotocol/sdk/client";
2
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
4
+ import type { McpServerConfig } from "./httpClient";
5
+
6
+ const pool = new Map<string, Client>();
7
+
8
+ function keyOf(server: McpServerConfig) {
9
+ const headers = Object.entries(server.headers ?? {})
10
+ .sort(([a], [b]) => a.localeCompare(b))
11
+ .map(([k, v]) => `${k}:${v}`)
12
+ .join("|\u0000|");
13
+ return `${server.url}|${headers}`;
14
+ }
15
+
16
+ export async function getClient(server: McpServerConfig, signal?: AbortSignal): Promise<Client> {
17
+ const key = keyOf(server);
18
+ const existing = pool.get(key);
19
+ if (existing) return existing;
20
+
21
+ const client = new Client({ name: "chat-ui-mcp", version: "0.1.0" });
22
+ const url = new URL(server.url);
23
+ const requestInit: RequestInit = { headers: server.headers, signal };
24
+ try {
25
+ try {
26
+ await client.connect(new StreamableHTTPClientTransport(url, { requestInit }));
27
+ } catch {
28
+ await client.connect(new SSEClientTransport(url, { requestInit }));
29
+ }
30
+ } catch (err) {
31
+ try {
32
+ await client.close?.();
33
+ } catch {}
34
+ throw err;
35
+ }
36
+
37
+ pool.set(key, client);
38
+ return client;
39
+ }
40
+
41
+ export async function drainPool() {
42
+ for (const [key, client] of pool) {
43
+ try {
44
+ await client.close?.();
45
+ } catch {}
46
+ pool.delete(key);
47
+ }
48
+ }
src/lib/server/mcp/hf.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Minimal shared helpers for HF MCP token forwarding
2
+
3
+ export const hasAuthHeader = (h?: Record<string, string>) =>
4
+ !!h && Object.keys(h).some((k) => k.toLowerCase() === "authorization");
5
+
6
+ export const isStrictHfMcpLogin = (urlString: string) => {
7
+ try {
8
+ const u = new URL(urlString);
9
+ return (
10
+ u.protocol === "https:" &&
11
+ u.hostname === "huggingface.co" &&
12
+ u.pathname === "/mcp" &&
13
+ u.search === "?login"
14
+ );
15
+ } catch {
16
+ return false;
17
+ }
18
+ };
19
+
20
+ export const hasNonEmptyToken = (tok: unknown): tok is string =>
21
+ typeof tok === "string" && tok.trim().length > 0;
src/lib/server/mcp/httpClient.ts ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Client } from "@modelcontextprotocol/sdk/client";
2
+ import { getClient } from "./clientPool";
3
+
4
+ export interface McpServerConfig {
5
+ name: string;
6
+ url: string;
7
+ headers?: Record<string, string>;
8
+ }
9
+
10
+ const DEFAULT_TIMEOUT_MS = 30_000;
11
+
12
+ export type McpToolTextResponse = {
13
+ text: string;
14
+ /** If the server returned structuredContent, include it raw */
15
+ structured?: unknown;
16
+ /** Raw content blocks returned by the server, if any */
17
+ content?: unknown[];
18
+ };
19
+
20
+ export async function callMcpTool(
21
+ server: McpServerConfig,
22
+ tool: string,
23
+ args: unknown = {},
24
+ {
25
+ timeoutMs = DEFAULT_TIMEOUT_MS,
26
+ signal,
27
+ client,
28
+ }: { timeoutMs?: number; signal?: AbortSignal; client?: Client } = {}
29
+ ): Promise<McpToolTextResponse> {
30
+ const normalizedArgs =
31
+ typeof args === "object" && args !== null && !Array.isArray(args)
32
+ ? (args as Record<string, unknown>)
33
+ : undefined;
34
+
35
+ // Get a (possibly pooled) client. The client itself was connected with a signal
36
+ // that already composes outer cancellation. We still enforce a per-call timeout here.
37
+ const activeClient = client ?? (await getClient(server, signal));
38
+
39
+ // Prefer the SDK's built-in request controls (timeout, signal)
40
+ const response = await activeClient.callTool(
41
+ { name: tool, arguments: normalizedArgs },
42
+ undefined,
43
+ { signal, timeout: timeoutMs }
44
+ );
45
+
46
+ const parts = Array.isArray(response?.content) ? (response.content as Array<unknown>) : [];
47
+ const textParts = parts
48
+ .filter((part): part is { type: "text"; text: string } => {
49
+ if (typeof part !== "object" || part === null) return false;
50
+ const obj = part as Record<string, unknown>;
51
+ return obj["type"] === "text" && typeof obj["text"] === "string";
52
+ })
53
+ .map((p) => p.text);
54
+
55
+ const text = textParts.join("\n");
56
+ const structured = (response as unknown as { structuredContent?: unknown })?.structuredContent;
57
+ const contentBlocks = Array.isArray(response?.content)
58
+ ? (response.content as unknown[])
59
+ : undefined;
60
+ return { text, structured, content: contentBlocks };
61
+ }
src/lib/server/mcp/registry.ts ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { config } from "$lib/server/config";
2
+ import { logger } from "$lib/server/logger";
3
+ import type { McpServerConfig } from "./httpClient";
4
+ import { resetMcpToolsCache } from "./tools";
5
+
6
+ let cachedRaw: string | null = null;
7
+ let cachedServers: McpServerConfig[] = [];
8
+
9
+ function parseServers(raw: string): McpServerConfig[] {
10
+ if (!raw) return [];
11
+
12
+ try {
13
+ const parsed = JSON.parse(raw);
14
+ if (!Array.isArray(parsed)) return [];
15
+
16
+ return parsed
17
+ .map((entry) => {
18
+ if (!entry || typeof entry !== "object") return undefined;
19
+ const name = (entry as Record<string, unknown>).name;
20
+ const url = (entry as Record<string, unknown>).url;
21
+ if (typeof name !== "string" || !name.trim()) return undefined;
22
+ if (typeof url !== "string" || !url.trim()) return undefined;
23
+
24
+ const headersRaw = (entry as Record<string, unknown>).headers;
25
+ let headers: Record<string, string> | undefined;
26
+ if (headersRaw && typeof headersRaw === "object" && !Array.isArray(headersRaw)) {
27
+ const headerEntries = Object.entries(headersRaw as Record<string, unknown>).filter(
28
+ (entry): entry is [string, string] => typeof entry[1] === "string"
29
+ );
30
+ headers = Object.fromEntries(headerEntries);
31
+ }
32
+
33
+ return headers ? { name, url, headers } : { name, url };
34
+ })
35
+ .filter((server): server is McpServerConfig => Boolean(server));
36
+ } catch (error) {
37
+ logger.warn({ err: error }, "[mcp] failed to parse MCP_SERVERS env");
38
+ return [];
39
+ }
40
+ }
41
+
42
+ function setServers(raw: string) {
43
+ cachedServers = parseServers(raw);
44
+ cachedRaw = raw;
45
+ resetMcpToolsCache();
46
+ logger.debug({ count: cachedServers.length }, "[mcp] loaded server configuration");
47
+ console.log(
48
+ `[MCP] Loaded ${cachedServers.length} server(s):`,
49
+ cachedServers.map((s) => s.name).join(", ") || "none"
50
+ );
51
+ }
52
+
53
+ export function loadMcpServersOnStartup(): McpServerConfig[] {
54
+ const raw = config.MCP_SERVERS || "[]";
55
+ setServers(raw);
56
+ return cachedServers;
57
+ }
58
+
59
+ export function refreshMcpServersIfChanged(): void {
60
+ const currentRaw = config.MCP_SERVERS || "[]";
61
+ if (cachedRaw === null) {
62
+ setServers(currentRaw);
63
+ return;
64
+ }
65
+
66
+ if (currentRaw !== cachedRaw) {
67
+ setServers(currentRaw);
68
+ }
69
+ }
70
+
71
+ export function getMcpServers(): McpServerConfig[] {
72
+ if (cachedRaw === null) {
73
+ loadMcpServersOnStartup();
74
+ }
75
+ return cachedServers;
76
+ }
src/lib/server/mcp/tools.ts ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Client } from "@modelcontextprotocol/sdk/client";
2
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
4
+ import type { McpServerConfig } from "./httpClient";
5
+
6
+ export type OpenAiTool = {
7
+ type: "function";
8
+ function: { name: string; description?: string; parameters?: Record<string, unknown> };
9
+ };
10
+
11
+ export interface McpToolMapping {
12
+ fnName: string;
13
+ server: string;
14
+ tool: string;
15
+ }
16
+
17
+ interface CacheEntry {
18
+ fetchedAt: number;
19
+ ttlMs: number;
20
+ tools: OpenAiTool[];
21
+ mapping: Record<string, McpToolMapping>;
22
+ }
23
+
24
+ const DEFAULT_TTL_MS = 60_000;
25
+ const cache = new Map<string, CacheEntry>();
26
+
27
+ // Per OpenAI tool/function name guidelines most providers enforce:
28
+ // ^[a-zA-Z0-9_-]{1,64}$
29
+ // Dots are not universally accepted (e.g., MiniMax via HF router rejects them).
30
+ // Normalize any disallowed characters (including ".") to underscore and trim to 64 chars.
31
+ function sanitizeName(name: string) {
32
+ return name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
33
+ }
34
+
35
+ function buildCacheKey(servers: McpServerConfig[]): string {
36
+ const normalized = servers
37
+ .map((server) => ({
38
+ name: server.name,
39
+ url: server.url,
40
+ headers: server.headers
41
+ ? Object.entries(server.headers)
42
+ .sort(([a], [b]) => a.localeCompare(b))
43
+ .map(([key, value]) => [key, value])
44
+ : [],
45
+ }))
46
+ .sort((a, b) => {
47
+ const byName = a.name.localeCompare(b.name);
48
+ if (byName !== 0) return byName;
49
+ return a.url.localeCompare(b.url);
50
+ });
51
+
52
+ return JSON.stringify(normalized);
53
+ }
54
+
55
+ type ListedTool = {
56
+ name?: string;
57
+ inputSchema?: Record<string, unknown>;
58
+ description?: string;
59
+ annotations?: { title?: string };
60
+ };
61
+
62
+ async function listServerTools(
63
+ server: McpServerConfig,
64
+ opts: { signal?: AbortSignal } = {}
65
+ ): Promise<ListedTool[]> {
66
+ const url = new URL(server.url);
67
+ const client = new Client({ name: "chat-ui-mcp", version: "0.1.0" });
68
+ try {
69
+ try {
70
+ const transport = new StreamableHTTPClientTransport(url, {
71
+ requestInit: { headers: server.headers, signal: opts.signal },
72
+ });
73
+ await client.connect(transport);
74
+ } catch {
75
+ const transport = new SSEClientTransport(url, {
76
+ requestInit: { headers: server.headers, signal: opts.signal },
77
+ });
78
+ await client.connect(transport);
79
+ }
80
+
81
+ const response = await client.listTools({});
82
+ return Array.isArray(response?.tools) ? (response.tools as ListedTool[]) : [];
83
+ } finally {
84
+ try {
85
+ await client.close?.();
86
+ } catch {
87
+ // ignore close errors
88
+ }
89
+ }
90
+ }
91
+
92
+ export async function getOpenAiToolsForMcp(
93
+ servers: McpServerConfig[],
94
+ { ttlMs = DEFAULT_TTL_MS, signal }: { ttlMs?: number; signal?: AbortSignal } = {}
95
+ ): Promise<{ tools: OpenAiTool[]; mapping: Record<string, McpToolMapping> }> {
96
+ const now = Date.now();
97
+ const cacheKey = buildCacheKey(servers);
98
+ const cached = cache.get(cacheKey);
99
+ if (cached && now - cached.fetchedAt < cached.ttlMs) {
100
+ return { tools: cached.tools, mapping: cached.mapping };
101
+ }
102
+
103
+ const tools: OpenAiTool[] = [];
104
+ const mapping: Record<string, McpToolMapping> = {};
105
+
106
+ const seenNames = new Set<string>();
107
+
108
+ const pushToolDefinition = (
109
+ name: string,
110
+ description: string | undefined,
111
+ parameters: Record<string, unknown> | undefined
112
+ ) => {
113
+ if (seenNames.has(name)) return;
114
+ tools.push({
115
+ type: "function",
116
+ function: {
117
+ name,
118
+ description,
119
+ parameters,
120
+ },
121
+ });
122
+ seenNames.add(name);
123
+ };
124
+
125
+ // Fetch tools in parallel; tolerate individual failures
126
+ const tasks = servers.map((server) => listServerTools(server, { signal }));
127
+ const results = await Promise.allSettled(tasks);
128
+
129
+ for (let i = 0; i < results.length; i++) {
130
+ const server = servers[i];
131
+ const r = results[i];
132
+ if (r.status === "fulfilled") {
133
+ const serverTools = r.value;
134
+ for (const tool of serverTools) {
135
+ if (typeof tool.name !== "string" || tool.name.trim().length === 0) {
136
+ continue;
137
+ }
138
+
139
+ const parameters =
140
+ tool.inputSchema && typeof tool.inputSchema === "object" ? tool.inputSchema : undefined;
141
+ const description = tool.description ?? tool.annotations?.title;
142
+ const toolName = tool.name;
143
+
144
+ // Emit a collision-aware function name.
145
+ // Prefer the plain tool name; on conflict, suffix with server name.
146
+ let plainName = sanitizeName(toolName);
147
+ if (plainName in mapping) {
148
+ const suffix = sanitizeName(server.name);
149
+ const candidate = `${plainName}_${suffix}`.slice(0, 64);
150
+ if (!(candidate in mapping)) {
151
+ plainName = candidate;
152
+ } else {
153
+ let i = 2;
154
+ let next = `${candidate}_${i}`;
155
+ while (i < 10 && next in mapping) {
156
+ i += 1;
157
+ next = `${candidate}_${i}`;
158
+ }
159
+ plainName = next.slice(0, 64);
160
+ }
161
+ }
162
+
163
+ pushToolDefinition(plainName, description, parameters);
164
+ mapping[plainName] = {
165
+ fnName: plainName,
166
+ server: server.name,
167
+ tool: toolName,
168
+ };
169
+ }
170
+ } else {
171
+ // ignore failure for this server
172
+ continue;
173
+ }
174
+ }
175
+
176
+ cache.set(cacheKey, { fetchedAt: now, ttlMs, tools, mapping });
177
+ return { tools, mapping };
178
+ }
179
+
180
+ export function resetMcpToolsCache() {
181
+ cache.clear();
182
+ }
src/lib/server/models.ts CHANGED
@@ -56,6 +56,8 @@ const modelConfig = z.object({
56
  .optional(),
57
  multimodal: z.boolean().default(false),
58
  multimodalAcceptedMimetypes: z.array(z.string()).optional(),
 
 
59
  unlisted: z.boolean().default(false),
60
  embeddingModel: z.never().optional(),
61
  /** Used to enable/disable system prompt usage */
@@ -234,6 +236,7 @@ const signatureForModel = (model: ProcessedModel) =>
234
  }) ?? null,
235
  multimodal: model.multimodal,
236
  multimodalAcceptedMimetypes: model.multimodalAcceptedMimetypes,
 
237
  isRouter: model.isRouter,
238
  hasInferenceAPI: model.hasInferenceAPI,
239
  });
@@ -341,6 +344,9 @@ const buildModels = async (): Promise<ProcessedModel[]> => {
341
  );
342
  const supportsImageInput =
343
  inputModalities.includes("image") || inputModalities.includes("vision");
 
 
 
344
  return {
345
  id: m.id,
346
  name: m.id,
@@ -350,6 +356,7 @@ const buildModels = async (): Promise<ProcessedModel[]> => {
350
  providers: m.providers,
351
  multimodal: supportsImageInput,
352
  multimodalAcceptedMimetypes: supportsImageInput ? ["image/*"] : undefined,
 
353
  endpoints: [
354
  {
355
  type: "openai" as const,
@@ -405,6 +412,7 @@ const buildModels = async (): Promise<ProcessedModel[]> => {
405
  const routerAliasId = (config.PUBLIC_LLM_ROUTER_ALIAS_ID || "omni").trim() || "omni";
406
  const routerMultimodalEnabled =
407
  (config.LLM_ROUTER_ENABLE_MULTIMODAL || "").toLowerCase() === "true";
 
408
 
409
  let decorated = builtModels as ProcessedModel[];
410
 
@@ -432,6 +440,10 @@ const buildModels = async (): Promise<ProcessedModel[]> => {
432
  aliasRaw.multimodalAcceptedMimetypes = ["image/*"];
433
  }
434
 
 
 
 
 
435
  const aliasBase = await processModel(aliasRaw);
436
  // Create a self-referential ProcessedModel for the router endpoint
437
  const aliasModel: ProcessedModel = {
 
56
  .optional(),
57
  multimodal: z.boolean().default(false),
58
  multimodalAcceptedMimetypes: z.array(z.string()).optional(),
59
+ // Aggregated tool-calling capability across providers (HF router)
60
+ supportsTools: z.boolean().default(false),
61
  unlisted: z.boolean().default(false),
62
  embeddingModel: z.never().optional(),
63
  /** Used to enable/disable system prompt usage */
 
236
  }) ?? null,
237
  multimodal: model.multimodal,
238
  multimodalAcceptedMimetypes: model.multimodalAcceptedMimetypes,
239
+ supportsTools: (model as unknown as { supportsTools?: boolean }).supportsTools ?? false,
240
  isRouter: model.isRouter,
241
  hasInferenceAPI: model.hasInferenceAPI,
242
  });
 
344
  );
345
  const supportsImageInput =
346
  inputModalities.includes("image") || inputModalities.includes("vision");
347
+
348
+ // If any provider supports tools, consider the model as supporting tools
349
+ const supportsTools = Boolean((m.providers ?? []).some((p) => p?.supports_tools === true));
350
  return {
351
  id: m.id,
352
  name: m.id,
 
356
  providers: m.providers,
357
  multimodal: supportsImageInput,
358
  multimodalAcceptedMimetypes: supportsImageInput ? ["image/*"] : undefined,
359
+ supportsTools,
360
  endpoints: [
361
  {
362
  type: "openai" as const,
 
412
  const routerAliasId = (config.PUBLIC_LLM_ROUTER_ALIAS_ID || "omni").trim() || "omni";
413
  const routerMultimodalEnabled =
414
  (config.LLM_ROUTER_ENABLE_MULTIMODAL || "").toLowerCase() === "true";
415
+ const routerToolsEnabled = (config.LLM_ROUTER_ENABLE_TOOLS || "").toLowerCase() === "true";
416
 
417
  let decorated = builtModels as ProcessedModel[];
418
 
 
440
  aliasRaw.multimodalAcceptedMimetypes = ["image/*"];
441
  }
442
 
443
+ if (routerToolsEnabled) {
444
+ aliasRaw.supportsTools = true;
445
+ }
446
+
447
  const aliasBase = await processModel(aliasRaw);
448
  // Create a self-referential ProcessedModel for the router endpoint
449
  const aliasModel: ProcessedModel = {
src/lib/server/router/endpoint.ts CHANGED
@@ -12,6 +12,12 @@ import { archSelectRoute } from "./arch";
12
  import { getRoutes, resolveRouteModels } from "./policy";
13
  import { getApiToken } from "$lib/server/apiToken";
14
  import { ROUTER_FAILURE } from "./types";
 
 
 
 
 
 
15
 
16
  const REASONING_BLOCK_REGEX = /<think>[\s\S]*?(?:<\/think>|$)/g;
17
 
@@ -115,11 +121,14 @@ export async function makeRouterEndpoint(routerModel: ProcessedModel): Promise<E
115
  const sanitizedMessages = params.messages.map(stripReasoningFromMessage);
116
  const routerMultimodalEnabled =
117
  (config.LLM_ROUTER_ENABLE_MULTIMODAL || "").toLowerCase() === "true";
 
118
  const hasImageInput = sanitizedMessages.some((message) =>
119
  (message.files ?? []).some(
120
  (file) => typeof file?.mime === "string" && file.mime.startsWith("image/")
121
  )
122
  );
 
 
123
 
124
  // Helper to create an OpenAI endpoint for a specific candidate model id
125
  async function createCandidateEndpoint(candidateModelId: string): Promise<Endpoint> {
@@ -230,6 +239,46 @@ export async function makeRouterEndpoint(routerModel: ProcessedModel): Promise<E
230
  }
231
  }
232
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  const routeSelection = await archSelectRoute(sanitizedMessages, undefined, params.locals);
234
 
235
  // If arch router failed with an error, only hard-fail for policy errors (402/401/403)
 
12
  import { getRoutes, resolveRouteModels } from "./policy";
13
  import { getApiToken } from "$lib/server/apiToken";
14
  import { ROUTER_FAILURE } from "./types";
15
+ import {
16
+ hasActiveToolsSelection,
17
+ isRouterToolsBypassEnabled,
18
+ pickToolsCapableModel,
19
+ ROUTER_TOOLS_ROUTE,
20
+ } from "./toolsRoute";
21
 
22
  const REASONING_BLOCK_REGEX = /<think>[\s\S]*?(?:<\/think>|$)/g;
23
 
 
121
  const sanitizedMessages = params.messages.map(stripReasoningFromMessage);
122
  const routerMultimodalEnabled =
123
  (config.LLM_ROUTER_ENABLE_MULTIMODAL || "").toLowerCase() === "true";
124
+ const routerToolsEnabled = isRouterToolsBypassEnabled();
125
  const hasImageInput = sanitizedMessages.some((message) =>
126
  (message.files ?? []).some(
127
  (file) => typeof file?.mime === "string" && file.mime.startsWith("image/")
128
  )
129
  );
130
+ // Tools are considered "active" if the client indicated any enabled MCP server
131
+ const hasToolsActive = hasActiveToolsSelection(params.locals);
132
 
133
  // Helper to create an OpenAI endpoint for a specific candidate model id
134
  async function createCandidateEndpoint(candidateModelId: string): Promise<Endpoint> {
 
239
  }
240
  }
241
 
242
+ async function findToolsCandidateModel(): Promise<ProcessedModel | undefined> {
243
+ try {
244
+ const all = await getModels();
245
+ return pickToolsCapableModel(all);
246
+ } catch (e) {
247
+ logger.warn({ err: String(e) }, "[router] failed to load models for tools lookup");
248
+ return undefined;
249
+ }
250
+ }
251
+
252
+ if (routerToolsEnabled && hasToolsActive) {
253
+ const toolsModel = await findToolsCandidateModel();
254
+ const toolsCandidate = toolsModel?.id ?? toolsModel?.name;
255
+ if (!toolsCandidate) {
256
+ // No tool-capable model found — continue with normal routing instead of hard failing
257
+ } else {
258
+ try {
259
+ logger.info(
260
+ { route: ROUTER_TOOLS_ROUTE, model: toolsCandidate },
261
+ "[router] tools active; bypassing Arch selection"
262
+ );
263
+ const ep = await createCandidateEndpoint(toolsCandidate);
264
+ const gen = await ep({ ...params });
265
+ return metadataThenStream(gen, toolsCandidate, ROUTER_TOOLS_ROUTE);
266
+ } catch (e) {
267
+ const { message, statusCode } = extractUpstreamError(e);
268
+ logger.error(
269
+ {
270
+ route: ROUTER_TOOLS_ROUTE,
271
+ model: toolsCandidate,
272
+ err: message,
273
+ ...(statusCode && { status: statusCode }),
274
+ },
275
+ "[router] tools fallback failed"
276
+ );
277
+ throw statusCode ? new HTTPError(message, statusCode) : new Error(message);
278
+ }
279
+ }
280
+ }
281
+
282
  const routeSelection = await archSelectRoute(sanitizedMessages, undefined, params.locals);
283
 
284
  // If arch router failed with an error, only hard-fail for policy errors (402/401/403)
src/lib/server/router/toolsRoute.ts ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { config } from "$lib/server/config";
2
+ import { logger } from "$lib/server/logger";
3
+ import type { ProcessedModel } from "../models";
4
+
5
+ export const ROUTER_TOOLS_ROUTE = "agentic";
6
+
7
+ type LocalsWithMcp = App.Locals & {
8
+ mcp?: {
9
+ selectedServers?: unknown[];
10
+ selectedServerNames?: unknown[];
11
+ };
12
+ };
13
+
14
+ export function isRouterToolsBypassEnabled(): boolean {
15
+ return (config.LLM_ROUTER_ENABLE_TOOLS || "").toLowerCase() === "true";
16
+ }
17
+
18
+ export function hasActiveToolsSelection(locals: App.Locals | undefined): boolean {
19
+ try {
20
+ const reqMcp = (locals as LocalsWithMcp | undefined)?.mcp;
21
+ const byConfig =
22
+ Array.isArray(reqMcp?.selectedServers) && (reqMcp?.selectedServers?.length ?? 0) > 0;
23
+ const byName =
24
+ Array.isArray(reqMcp?.selectedServerNames) && (reqMcp?.selectedServerNames?.length ?? 0) > 0;
25
+ return Boolean(byConfig || byName);
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ export function pickToolsCapableModel(
32
+ models: ProcessedModel[] | undefined
33
+ ): ProcessedModel | undefined {
34
+ const preferredRaw = (config as unknown as Record<string, string>).LLM_ROUTER_TOOLS_MODEL;
35
+ const preferred = preferredRaw?.trim();
36
+ if (!preferred) {
37
+ logger.warn("[router] tools bypass requested but LLM_ROUTER_TOOLS_MODEL is not set");
38
+ return undefined;
39
+ }
40
+ if (!models?.length) return undefined;
41
+ const found = models.find((m) => m.id === preferred || m.name === preferred);
42
+ if (!found) {
43
+ logger.warn(
44
+ { configuredModel: preferred },
45
+ "[router] configured tools model not found; falling back to Arch routing"
46
+ );
47
+ return undefined;
48
+ }
49
+ logger.info({ model: found.id ?? found.name }, "[router] using configured tools model");
50
+ return found;
51
+ }
src/lib/server/textGeneration/generate.ts CHANGED
@@ -1,7 +1,14 @@
1
- import { MessageUpdateType, type MessageUpdate } from "$lib/types/MessageUpdate";
 
 
 
 
 
2
  import { AbortedGenerations } from "../abortedGenerations";
3
  import type { TextGenerationContext } from "./types";
4
  import type { EndpointMessage } from "../endpoints/endpoints";
 
 
5
  import { logger } from "../logger";
6
 
7
  type GenerateContext = Omit<TextGenerationContext, "messages"> & { messages: EndpointMessage[] };
@@ -20,6 +27,30 @@ export async function* generate(
20
  }: GenerateContext,
21
  preprompt?: string
22
  ): AsyncIterable<MessageUpdate> {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  const stream = await endpoint({
24
  messages,
25
  preprompt,
@@ -32,20 +63,24 @@ export async function* generate(
32
  });
33
 
34
  for await (const output of stream) {
35
- // Check if this output contains router metadata
36
- if (
37
- "routerMetadata" in output &&
38
- output.routerMetadata &&
39
- ((output.routerMetadata.route && output.routerMetadata.model) ||
40
- output.routerMetadata.provider)
41
- ) {
42
- yield {
43
- type: MessageUpdateType.RouterMetadata,
44
- route: output.routerMetadata.route || "",
45
- model: output.routerMetadata.model || "",
46
- provider: output.routerMetadata.provider,
47
- };
48
- continue;
 
 
 
 
49
  }
50
  // text generation completed
51
  if (output.generated_text) {
@@ -60,19 +95,139 @@ export async function* generate(
60
  text = text.slice(0, text.length - stopToken.length);
61
  }
62
 
63
- yield {
64
- type: MessageUpdateType.FinalAnswer,
65
- text,
66
- interrupted,
67
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  continue;
69
  }
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  // ignore special tokens
72
  if (output.token.special) continue;
73
 
74
- // yield normal token
75
- yield { type: MessageUpdateType.Stream, token: output.token.text };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
  // abort check
78
  const date = AbortedGenerations.getInstance().getAbortTime(conv._id.toString());
 
1
+ import { config } from "$lib/server/config";
2
+ import {
3
+ MessageReasoningUpdateType,
4
+ MessageUpdateType,
5
+ type MessageUpdate,
6
+ } from "$lib/types/MessageUpdate";
7
  import { AbortedGenerations } from "../abortedGenerations";
8
  import type { TextGenerationContext } from "./types";
9
  import type { EndpointMessage } from "../endpoints/endpoints";
10
+ import { generateFromDefaultEndpoint } from "../generateFromDefaultEndpoint";
11
+ import { generateSummaryOfReasoning } from "./reasoning";
12
  import { logger } from "../logger";
13
 
14
  type GenerateContext = Omit<TextGenerationContext, "messages"> & { messages: EndpointMessage[] };
 
27
  }: GenerateContext,
28
  preprompt?: string
29
  ): AsyncIterable<MessageUpdate> {
30
+ // Reasoning mode support
31
+ let reasoning = false;
32
+ let reasoningBuffer = "";
33
+ let lastReasoningUpdate = new Date();
34
+ let status = "";
35
+ const startTime = new Date();
36
+ const modelReasoning = Reflect.get(model, "reasoning") as
37
+ | { type: string; beginToken?: string; endToken?: string; regex?: string }
38
+ | undefined;
39
+ if (
40
+ modelReasoning &&
41
+ (modelReasoning.type === "regex" ||
42
+ modelReasoning.type === "summarize" ||
43
+ (modelReasoning.type === "tokens" && modelReasoning.beginToken === ""))
44
+ ) {
45
+ // Starts in reasoning mode and we extract the answer from the reasoning
46
+ reasoning = true;
47
+ yield {
48
+ type: MessageUpdateType.Reasoning,
49
+ subtype: MessageReasoningUpdateType.Status,
50
+ status: "Started reasoning...",
51
+ };
52
+ }
53
+
54
  const stream = await endpoint({
55
  messages,
56
  preprompt,
 
63
  });
64
 
65
  for await (const output of stream) {
66
+ // Check if this output contains router metadata. Emit if either:
67
+ // 1) route+model are present (router models), or
68
+ // 2) provider-only is present (non-router models exposing x-inference-provider)
69
+ if ("routerMetadata" in output && output.routerMetadata) {
70
+ const hasRouteModel = Boolean(output.routerMetadata.route && output.routerMetadata.model);
71
+ const hasProviderOnly = Boolean(output.routerMetadata.provider);
72
+ if (hasRouteModel || hasProviderOnly) {
73
+ yield {
74
+ type: MessageUpdateType.RouterMetadata,
75
+ route: output.routerMetadata.route || "",
76
+ model: output.routerMetadata.model || "",
77
+ provider:
78
+ (output.routerMetadata
79
+ .provider as unknown as import("@huggingface/inference").InferenceProvider) ||
80
+ undefined,
81
+ };
82
+ continue;
83
+ }
84
  }
85
  // text generation completed
86
  if (output.generated_text) {
 
95
  text = text.slice(0, text.length - stopToken.length);
96
  }
97
 
98
+ let finalAnswer = text;
99
+ if (modelReasoning && modelReasoning.type === "regex" && modelReasoning.regex) {
100
+ const regex = new RegExp(modelReasoning.regex);
101
+ finalAnswer = regex.exec(reasoningBuffer)?.[1] ?? text;
102
+ } else if (modelReasoning && modelReasoning.type === "summarize") {
103
+ yield {
104
+ type: MessageUpdateType.Reasoning,
105
+ subtype: MessageReasoningUpdateType.Status,
106
+ status: "Summarizing reasoning...",
107
+ };
108
+ try {
109
+ const summary = yield* generateFromDefaultEndpoint({
110
+ messages: [
111
+ {
112
+ from: "user",
113
+ content: `Question: ${messages[messages.length - 1].content}\n\nReasoning: ${reasoningBuffer}`,
114
+ },
115
+ ],
116
+ preprompt: `Your task is to summarize concisely all your reasoning steps and then give the final answer. Keep it short, one short paragraph at most. If the reasoning steps explicitly include a code solution, make sure to include it in your answer.`,
117
+ modelId: Reflect.get(model, "id") as string | undefined,
118
+ locals,
119
+ });
120
+ finalAnswer = summary;
121
+ yield {
122
+ type: MessageUpdateType.Reasoning,
123
+ subtype: MessageReasoningUpdateType.Status,
124
+ status: `Done in ${Math.round((new Date().getTime() - startTime.getTime()) / 1000)}s.`,
125
+ };
126
+ } catch (e) {
127
+ finalAnswer = text;
128
+ logger.error(e);
129
+ }
130
+ } else if (modelReasoning && modelReasoning.type === "tokens") {
131
+ // Remove the reasoning segment from final answer to avoid duplication
132
+ const beginIndex = modelReasoning.beginToken
133
+ ? reasoningBuffer.indexOf(modelReasoning.beginToken)
134
+ : 0;
135
+ const endIndex = modelReasoning.endToken
136
+ ? reasoningBuffer.lastIndexOf(modelReasoning.endToken)
137
+ : -1;
138
+
139
+ if (beginIndex !== -1 && endIndex !== -1 && modelReasoning.endToken) {
140
+ finalAnswer =
141
+ text.slice(0, beginIndex) + text.slice(endIndex + modelReasoning.endToken.length);
142
+ }
143
+ }
144
+
145
+ yield { type: MessageUpdateType.FinalAnswer, text: finalAnswer, interrupted };
146
  continue;
147
  }
148
 
149
+ if (modelReasoning && modelReasoning.type === "tokens") {
150
+ if (output.token.text === modelReasoning.beginToken) {
151
+ reasoning = true;
152
+ reasoningBuffer += output.token.text;
153
+ continue;
154
+ } else if (modelReasoning.endToken && output.token.text === modelReasoning.endToken) {
155
+ reasoning = false;
156
+ reasoningBuffer += output.token.text;
157
+ yield {
158
+ type: MessageUpdateType.Reasoning,
159
+ subtype: MessageReasoningUpdateType.Status,
160
+ status: `Done in ${Math.round((new Date().getTime() - startTime.getTime()) / 1000)}s.`,
161
+ };
162
+ continue;
163
+ }
164
+ }
165
+
166
  // ignore special tokens
167
  if (output.token.special) continue;
168
 
169
+ // pass down normal token
170
+ if (reasoning) {
171
+ reasoningBuffer += output.token.text;
172
+
173
+ if (modelReasoning && modelReasoning.type === "tokens" && modelReasoning.endToken) {
174
+ if (reasoningBuffer.lastIndexOf(modelReasoning.endToken) !== -1) {
175
+ const endTokenIndex = reasoningBuffer.lastIndexOf(modelReasoning.endToken);
176
+ const textBuffer = reasoningBuffer.slice(endTokenIndex + modelReasoning.endToken.length);
177
+ reasoningBuffer = reasoningBuffer.slice(
178
+ 0,
179
+ endTokenIndex + modelReasoning.endToken.length + 1
180
+ );
181
+
182
+ yield {
183
+ type: MessageUpdateType.Reasoning,
184
+ subtype: MessageReasoningUpdateType.Stream,
185
+ token: output.token.text,
186
+ };
187
+ yield { type: MessageUpdateType.Stream, token: textBuffer };
188
+ yield {
189
+ type: MessageUpdateType.Reasoning,
190
+ subtype: MessageReasoningUpdateType.Status,
191
+ status: `Done in ${Math.round((new Date().getTime() - startTime.getTime()) / 1000)}s.`,
192
+ };
193
+ reasoning = false;
194
+ continue;
195
+ }
196
+ }
197
+
198
+ // yield status update if it has changed
199
+ if (status !== "") {
200
+ yield {
201
+ type: MessageUpdateType.Reasoning,
202
+ subtype: MessageReasoningUpdateType.Status,
203
+ status,
204
+ };
205
+ status = "";
206
+ }
207
+
208
+ // create a new status every ~4s (optional)
209
+ if (
210
+ Reflect.get(config, "REASONING_SUMMARY") === "true" &&
211
+ new Date().getTime() - lastReasoningUpdate.getTime() > 4000
212
+ ) {
213
+ lastReasoningUpdate = new Date();
214
+ try {
215
+ generateSummaryOfReasoning(reasoningBuffer, model.id, locals).then((summary) => {
216
+ status = summary;
217
+ });
218
+ } catch (e) {
219
+ logger.error(e);
220
+ }
221
+ }
222
+
223
+ yield {
224
+ type: MessageUpdateType.Reasoning,
225
+ subtype: MessageReasoningUpdateType.Stream,
226
+ token: output.token.text,
227
+ };
228
+ } else {
229
+ yield { type: MessageUpdateType.Stream, token: output.token.text };
230
+ }
231
 
232
  // abort check
233
  const date = AbortedGenerations.getInstance().getAbortTime(conv._id.toString());
src/lib/server/textGeneration/index.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
  MessageUpdateStatus,
8
  } from "$lib/types/MessageUpdate";
9
  import { generate } from "./generate";
 
10
  import { mergeAsyncGenerators } from "$lib/utils/mergeAsyncGenerators";
11
  import type { TextGenerationContext } from "./types";
12
 
@@ -47,6 +48,34 @@ async function* textGenerationWithoutTitle(
47
  const preprompt = conv.preprompt;
48
 
49
  const processedMessages = await preprocessMessages(messages, convId);
50
- yield* generate({ ...ctx, messages: processedMessages }, preprompt);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  done.abort();
52
  }
 
7
  MessageUpdateStatus,
8
  } from "$lib/types/MessageUpdate";
9
  import { generate } from "./generate";
10
+ import { runMcpFlow } from "./mcp/runMcpFlow";
11
  import { mergeAsyncGenerators } from "$lib/utils/mergeAsyncGenerators";
12
  import type { TextGenerationContext } from "./types";
13
 
 
48
  const preprompt = conv.preprompt;
49
 
50
  const processedMessages = await preprocessMessages(messages, convId);
51
+
52
+ // Try MCP tool flow first; fall back to default generation if not selected/available
53
+ try {
54
+ const mcpGen = runMcpFlow({
55
+ model: ctx.model,
56
+ conv,
57
+ messages: processedMessages,
58
+ assistant: ctx.assistant,
59
+ forceMultimodal: ctx.forceMultimodal,
60
+ forceTools: ctx.forceTools,
61
+ locals: ctx.locals,
62
+ preprompt,
63
+ abortSignal: ctx.abortController.signal,
64
+ });
65
+
66
+ let step = await mcpGen.next();
67
+ while (!step.done) {
68
+ yield step.value;
69
+ step = await mcpGen.next();
70
+ }
71
+ const didRunMcp = Boolean(step.value);
72
+ if (!didRunMcp) {
73
+ // fallback to normal text generation
74
+ yield* generate({ ...ctx, messages: processedMessages }, preprompt);
75
+ }
76
+ } catch {
77
+ // On any MCP error, fall back to normal generation
78
+ yield* generate({ ...ctx, messages: processedMessages }, preprompt);
79
+ }
80
  done.abort();
81
  }
src/lib/server/textGeneration/mcp/routerResolution.ts ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { config } from "$lib/server/config";
2
+ import { archSelectRoute } from "$lib/server/router/arch";
3
+ import { getRoutes, resolveRouteModels } from "$lib/server/router/policy";
4
+ import {
5
+ hasActiveToolsSelection,
6
+ isRouterToolsBypassEnabled,
7
+ pickToolsCapableModel,
8
+ ROUTER_TOOLS_ROUTE,
9
+ } from "$lib/server/router/toolsRoute";
10
+ import type { EndpointMessage } from "../../endpoints/endpoints";
11
+ import { stripReasoningFromMessageForRouting } from "../utils/routing";
12
+ import type { ProcessedModel } from "../../models";
13
+ import { logger } from "../../logger";
14
+
15
+ export interface RouterResolutionInput {
16
+ model: ProcessedModel;
17
+ messages: EndpointMessage[];
18
+ conversationId: string;
19
+ hasImageInput: boolean;
20
+ locals: App.Locals | undefined;
21
+ }
22
+
23
+ export interface RouterResolutionResult {
24
+ runMcp: boolean;
25
+ targetModel: ProcessedModel;
26
+ candidateModelId?: string;
27
+ resolvedRoute?: string;
28
+ }
29
+
30
+ export async function resolveRouterTarget({
31
+ model,
32
+ messages,
33
+ conversationId,
34
+ hasImageInput,
35
+ locals,
36
+ }: RouterResolutionInput): Promise<RouterResolutionResult> {
37
+ let targetModel = model;
38
+ let candidateModelId: string | undefined;
39
+ let resolvedRoute: string | undefined;
40
+ let runMcp = true;
41
+
42
+ if (!model.isRouter) {
43
+ return { runMcp, targetModel };
44
+ }
45
+
46
+ try {
47
+ const mod = await import("../../models");
48
+ const allModels = mod.models as ProcessedModel[];
49
+
50
+ if (hasImageInput) {
51
+ const multimodalCandidate = allModels?.find(
52
+ (candidate) => !candidate.isRouter && candidate.multimodal
53
+ );
54
+ if (multimodalCandidate) {
55
+ targetModel = multimodalCandidate;
56
+ candidateModelId = multimodalCandidate.id ?? multimodalCandidate.name;
57
+ resolvedRoute = "multimodal";
58
+ } else {
59
+ runMcp = false;
60
+ }
61
+ } else {
62
+ // If tools are enabled and at least one MCP server is active, prefer a tools-capable model
63
+ const toolsEnabled = isRouterToolsBypassEnabled();
64
+ const hasToolsActive = hasActiveToolsSelection(locals);
65
+
66
+ if (toolsEnabled && hasToolsActive) {
67
+ const found = pickToolsCapableModel(allModels);
68
+ if (found) {
69
+ targetModel = found;
70
+ candidateModelId = found.id ?? found.name;
71
+ resolvedRoute = ROUTER_TOOLS_ROUTE;
72
+ // Continue; runMcp remains true
73
+ return { runMcp, targetModel, candidateModelId, resolvedRoute };
74
+ }
75
+ // No tools-capable model found; fall back to normal Arch routing below
76
+ }
77
+ const routes = await getRoutes();
78
+ const sanitized = messages.map(stripReasoningFromMessageForRouting);
79
+ const { routeName } = await archSelectRoute(sanitized, conversationId, locals);
80
+ resolvedRoute = routeName;
81
+ const fallbackModel = config.LLM_ROUTER_FALLBACK_MODEL || model.id;
82
+ const { candidates } = resolveRouteModels(routeName, routes, fallbackModel);
83
+ const primaryCandidateId = candidates[0];
84
+ if (!primaryCandidateId || primaryCandidateId === fallbackModel) {
85
+ runMcp = false;
86
+ } else {
87
+ const found = allModels?.find(
88
+ (candidate) =>
89
+ candidate.id === primaryCandidateId || candidate.name === primaryCandidateId
90
+ );
91
+ if (found) {
92
+ targetModel = found;
93
+ candidateModelId = primaryCandidateId;
94
+ } else {
95
+ runMcp = false;
96
+ }
97
+ }
98
+ }
99
+ } catch (error) {
100
+ logger.warn({ err: String(error) }, "[mcp] routing preflight failed");
101
+ runMcp = false;
102
+ }
103
+
104
+ return { runMcp, targetModel, candidateModelId, resolvedRoute };
105
+ }
src/lib/server/textGeneration/mcp/runMcpFlow.ts ADDED
@@ -0,0 +1,554 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { config } from "$lib/server/config";
2
+ import { MessageUpdateType, type MessageUpdate } from "$lib/types/MessageUpdate";
3
+ import type { EndpointMessage } from "../../endpoints/endpoints";
4
+ import { getMcpServers } from "$lib/server/mcp/registry";
5
+ import { isValidUrl } from "$lib/server/urlSafety";
6
+ import { resetMcpToolsCache } from "$lib/server/mcp/tools";
7
+ import { getOpenAiToolsForMcp } from "$lib/server/mcp/tools";
8
+ import type {
9
+ ChatCompletionChunk,
10
+ ChatCompletionCreateParamsStreaming,
11
+ ChatCompletionMessageParam,
12
+ ChatCompletionContentPart,
13
+ ChatCompletionMessageToolCall,
14
+ } from "openai/resources/chat/completions";
15
+ import type { Stream } from "openai/streaming";
16
+ import { buildToolPreprompt } from "../utils/toolPrompt";
17
+ import { resolveRouterTarget } from "./routerResolution";
18
+ import { executeToolCalls, type NormalizedToolCall } from "./toolInvocation";
19
+ import { drainPool } from "$lib/server/mcp/clientPool";
20
+ import { logger } from "../../logger";
21
+ import type { TextGenerationContext } from "../types";
22
+ import { hasAuthHeader, isStrictHfMcpLogin, hasNonEmptyToken } from "$lib/server/mcp/hf";
23
+
24
+ export type RunMcpFlowContext = Pick<
25
+ TextGenerationContext,
26
+ "model" | "conv" | "assistant" | "forceMultimodal" | "forceTools" | "locals"
27
+ > & { messages: EndpointMessage[] };
28
+
29
+ export async function* runMcpFlow({
30
+ model,
31
+ conv,
32
+ messages,
33
+ assistant,
34
+ forceMultimodal,
35
+ forceTools,
36
+ locals,
37
+ preprompt,
38
+ abortSignal,
39
+ }: RunMcpFlowContext & { preprompt?: string; abortSignal?: AbortSignal }): AsyncGenerator<
40
+ MessageUpdate,
41
+ boolean,
42
+ undefined
43
+ > {
44
+ // Start from env-configured servers
45
+ let servers = getMcpServers();
46
+
47
+ // Merge in request-provided custom servers (if any)
48
+ try {
49
+ const reqMcp = (
50
+ locals as unknown as {
51
+ mcp?: {
52
+ selectedServers?: Array<{ name: string; url: string; headers?: Record<string, string> }>;
53
+ selectedServerNames?: string[];
54
+ };
55
+ }
56
+ )?.mcp;
57
+ const custom = Array.isArray(reqMcp?.selectedServers) ? reqMcp?.selectedServers : [];
58
+ if (custom.length > 0) {
59
+ // Invalidate cached tool list when the set of servers changes at request-time
60
+ resetMcpToolsCache();
61
+ // Deduplicate by server name (request takes precedence)
62
+ const byName = new Map<
63
+ string,
64
+ { name: string; url: string; headers?: Record<string, string> }
65
+ >();
66
+ for (const s of servers) byName.set(s.name, s);
67
+ for (const s of custom) byName.set(s.name, s);
68
+ servers = [...byName.values()];
69
+ }
70
+
71
+ // If the client specified a selection by name, filter to those
72
+ const names = Array.isArray(reqMcp?.selectedServerNames)
73
+ ? reqMcp?.selectedServerNames
74
+ : undefined;
75
+ if (Array.isArray(names)) {
76
+ servers = servers.filter((s) => names.includes(s.name));
77
+ }
78
+ } catch {
79
+ // ignore selection merge errors and proceed with env servers
80
+ }
81
+
82
+ // Enforce server-side safety (public HTTPS only, no private ranges)
83
+ servers = servers.filter((s) => {
84
+ try {
85
+ return isValidUrl(s.url);
86
+ } catch {
87
+ return false;
88
+ }
89
+ });
90
+ if (servers.length === 0) {
91
+ logger.warn("[mcp] all selected servers rejected by URL safety guard");
92
+ return false;
93
+ }
94
+
95
+ // Optionally attach the logged-in user's HF token to the official HF MCP server only.
96
+ // Never override an explicit Authorization header, and require token to look like an HF token.
97
+ try {
98
+ const shouldForward = config.MCP_FORWARD_HF_USER_TOKEN === "true";
99
+ const userToken =
100
+ (locals as unknown as { hfAccessToken?: string } | undefined)?.hfAccessToken ??
101
+ (locals as unknown as { token?: string } | undefined)?.token;
102
+
103
+ if (shouldForward && hasNonEmptyToken(userToken)) {
104
+ servers = servers.map((s) => {
105
+ try {
106
+ if (isStrictHfMcpLogin(s.url) && !hasAuthHeader(s.headers)) {
107
+ return {
108
+ ...s,
109
+ headers: { ...(s.headers ?? {}), Authorization: `Bearer ${userToken}` },
110
+ };
111
+ }
112
+ } catch {
113
+ // ignore URL parse errors and leave server unchanged
114
+ }
115
+ return s;
116
+ });
117
+ }
118
+ } catch {
119
+ // best-effort overlay; continue if anything goes wrong
120
+ }
121
+ logger.debug({ count: servers.length }, "[mcp] servers configured");
122
+ if (servers.length === 0) {
123
+ return false;
124
+ }
125
+
126
+ // Gate MCP flow based on model tool support (aggregated) with user override
127
+ try {
128
+ const supportsTools = Boolean((model as unknown as { supportsTools?: boolean }).supportsTools);
129
+ const toolsEnabled = Boolean(forceTools) || supportsTools;
130
+ if (!toolsEnabled) {
131
+ logger.debug({ model: model.id }, "[mcp] tools disabled for model; skipping MCP flow");
132
+ return false;
133
+ }
134
+ } catch {
135
+ // If anything goes wrong reading the flag, proceed (previous behavior)
136
+ }
137
+
138
+ const hasImageInput = messages.some((msg) =>
139
+ (msg.files ?? []).some(
140
+ (file) => typeof file?.mime === "string" && file.mime.startsWith("image/")
141
+ )
142
+ );
143
+
144
+ const { runMcp, targetModel, candidateModelId, resolvedRoute } = await resolveRouterTarget({
145
+ model,
146
+ messages,
147
+ conversationId: conv._id.toString(),
148
+ hasImageInput,
149
+ locals,
150
+ });
151
+
152
+ if (!runMcp) {
153
+ logger.debug("[mcp] runMcp=false (router did not select tools path)");
154
+ return false;
155
+ }
156
+
157
+ const { tools: oaTools, mapping } = await getOpenAiToolsForMcp(servers, { signal: abortSignal });
158
+ logger.debug({ tools: oaTools.length }, "[mcp] openai tool defs built");
159
+ if (oaTools.length === 0) {
160
+ return false;
161
+ }
162
+
163
+ try {
164
+ const { OpenAI } = await import("openai");
165
+
166
+ // Capture provider header (x-inference-provider) from the upstream OpenAI-compatible server.
167
+ let providerHeader: string | undefined;
168
+ const captureProviderFetch = async (
169
+ input: RequestInfo | URL,
170
+ init?: RequestInit
171
+ ): Promise<Response> => {
172
+ const res = await fetch(input, init);
173
+ const p = res.headers.get("x-inference-provider");
174
+ if (p && !providerHeader) providerHeader = p;
175
+ return res;
176
+ };
177
+
178
+ const openai = new OpenAI({
179
+ apiKey: config.OPENAI_API_KEY || config.HF_TOKEN || "sk-",
180
+ baseURL: config.OPENAI_BASE_URL,
181
+ fetch: captureProviderFetch,
182
+ });
183
+
184
+ const mmEnabled = (forceMultimodal ?? false) || targetModel.multimodal;
185
+ logger.debug({ model: targetModel.id ?? targetModel.name, mmEnabled }, "[mcp] target model");
186
+ const toOpenAiMessage = (msg: EndpointMessage): ChatCompletionMessageParam => {
187
+ if (msg.from === "user" && mmEnabled) {
188
+ const parts: ChatCompletionContentPart[] = [{ type: "text", text: msg.content }];
189
+ for (const file of msg.files ?? []) {
190
+ if (typeof file?.mime === "string" && file.mime.startsWith("image/")) {
191
+ const rawValue = file.value as unknown;
192
+ let encoded: string;
193
+ if (typeof rawValue === "string") {
194
+ encoded = rawValue;
195
+ } else if (rawValue instanceof Uint8Array) {
196
+ encoded = Buffer.from(rawValue).toString("base64");
197
+ } else if (rawValue instanceof ArrayBuffer) {
198
+ encoded = Buffer.from(rawValue).toString("base64");
199
+ } else {
200
+ encoded = String(rawValue ?? "");
201
+ }
202
+ const url = encoded.startsWith("data:")
203
+ ? encoded
204
+ : `data:${file.mime};base64,${encoded}`;
205
+ parts.push({ type: "image_url", image_url: { url, detail: "auto" } });
206
+ }
207
+ }
208
+ return { role: msg.from, content: parts };
209
+ }
210
+ return { role: msg.from, content: msg.content };
211
+ };
212
+
213
+ let messagesOpenAI: ChatCompletionMessageParam[] = messages.map(toOpenAiMessage);
214
+ const toolPreprompt = buildToolPreprompt(oaTools);
215
+ const prepromptPieces: string[] = [];
216
+ if (toolPreprompt.trim().length > 0) {
217
+ prepromptPieces.push(toolPreprompt);
218
+ }
219
+ if (typeof preprompt === "string" && preprompt.trim().length > 0) {
220
+ prepromptPieces.push(preprompt);
221
+ }
222
+ const mergedPreprompt = prepromptPieces.join("\n\n");
223
+ const hasSystemMessage = messagesOpenAI.length > 0 && messagesOpenAI[0]?.role === "system";
224
+ if (hasSystemMessage) {
225
+ if (mergedPreprompt.length > 0) {
226
+ const existing = messagesOpenAI[0].content ?? "";
227
+ const existingText = typeof existing === "string" ? existing : "";
228
+ messagesOpenAI[0].content = mergedPreprompt + (existingText ? "\n\n" + existingText : "");
229
+ }
230
+ } else if (mergedPreprompt.length > 0) {
231
+ messagesOpenAI = [{ role: "system", content: mergedPreprompt }, ...messagesOpenAI];
232
+ }
233
+
234
+ // Work around servers that reject `system` role
235
+ if (
236
+ typeof config.OPENAI_BASE_URL === "string" &&
237
+ config.OPENAI_BASE_URL.length > 0 &&
238
+ (config.OPENAI_BASE_URL.includes("hf.space") ||
239
+ config.OPENAI_BASE_URL.includes("gradio.app")) &&
240
+ messagesOpenAI[0]?.role === "system"
241
+ ) {
242
+ messagesOpenAI[0] = { ...messagesOpenAI[0], role: "user" };
243
+ }
244
+
245
+ const parameters = { ...targetModel.parameters, ...assistant?.generateSettings } as Record<
246
+ string,
247
+ unknown
248
+ >;
249
+ const maxTokens =
250
+ (parameters?.max_tokens as number | undefined) ??
251
+ (parameters?.max_new_tokens as number | undefined) ??
252
+ (parameters?.max_completion_tokens as number | undefined);
253
+
254
+ const stopSequences =
255
+ typeof parameters?.stop === "string"
256
+ ? parameters.stop
257
+ : Array.isArray(parameters?.stop)
258
+ ? (parameters.stop as string[])
259
+ : undefined;
260
+
261
+ const completionBase: Omit<ChatCompletionCreateParamsStreaming, "messages"> = {
262
+ model: targetModel.id ?? targetModel.name,
263
+ stream: true,
264
+ temperature: typeof parameters?.temperature === "number" ? parameters.temperature : undefined,
265
+ top_p: typeof parameters?.top_p === "number" ? parameters.top_p : undefined,
266
+ frequency_penalty:
267
+ typeof parameters?.frequency_penalty === "number"
268
+ ? parameters.frequency_penalty
269
+ : typeof parameters?.repetition_penalty === "number"
270
+ ? parameters.repetition_penalty
271
+ : undefined,
272
+ presence_penalty:
273
+ typeof parameters?.presence_penalty === "number" ? parameters.presence_penalty : undefined,
274
+ stop: stopSequences,
275
+ max_tokens: typeof maxTokens === "number" ? maxTokens : undefined,
276
+ tools: oaTools,
277
+ tool_choice: "auto",
278
+ };
279
+
280
+ const toPrimitive = (value: unknown) => {
281
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
282
+ return value;
283
+ }
284
+ return undefined;
285
+ };
286
+
287
+ const parseArgs = (raw: unknown): Record<string, unknown> => {
288
+ if (typeof raw !== "string" || raw.trim().length === 0) return {};
289
+ try {
290
+ return JSON.parse(raw);
291
+ } catch {
292
+ return {};
293
+ }
294
+ };
295
+
296
+ const processToolOutput = (
297
+ text: string
298
+ ): {
299
+ annotated: string;
300
+ sources: { index: number; link: string }[];
301
+ } => ({ annotated: text, sources: [] });
302
+
303
+ let lastAssistantContent = "";
304
+ let streamedContent = false;
305
+ // Track whether we're inside a <think> block when the upstream streams
306
+ // provider-specific reasoning tokens (e.g. `reasoning` or `reasoning_content`).
307
+ let thinkOpen = false;
308
+
309
+ if (resolvedRoute && candidateModelId) {
310
+ yield {
311
+ type: MessageUpdateType.RouterMetadata,
312
+ route: resolvedRoute,
313
+ model: candidateModelId,
314
+ };
315
+ logger.debug(
316
+ { route: resolvedRoute, model: candidateModelId },
317
+ "[mcp] router metadata emitted"
318
+ );
319
+ }
320
+
321
+ for (let loop = 0; loop < 10; loop += 1) {
322
+ lastAssistantContent = "";
323
+ streamedContent = false;
324
+
325
+ const completionRequest: ChatCompletionCreateParamsStreaming = {
326
+ ...completionBase,
327
+ messages: messagesOpenAI,
328
+ };
329
+
330
+ const completionStream: Stream<ChatCompletionChunk> = await openai.chat.completions.create(
331
+ completionRequest,
332
+ {
333
+ signal: abortSignal,
334
+ headers: {
335
+ "ChatUI-Conversation-ID": conv._id.toString(),
336
+ "X-use-cache": "false",
337
+ },
338
+ }
339
+ );
340
+
341
+ // If provider header was exposed, notify UI so it can render "via {provider}".
342
+ if (providerHeader) {
343
+ yield {
344
+ type: MessageUpdateType.RouterMetadata,
345
+ route: "",
346
+ model: "",
347
+ provider: providerHeader as unknown as import("@huggingface/inference").InferenceProvider,
348
+ };
349
+ logger.debug({ provider: providerHeader }, "[mcp] provider metadata emitted");
350
+ }
351
+
352
+ const toolCallState: Record<number, { id?: string; name?: string; arguments: string }> = {};
353
+ let sawToolCall = false;
354
+ let tokenCount = 0;
355
+ for await (const chunk of completionStream) {
356
+ const choice = chunk.choices?.[0];
357
+ const delta = choice?.delta;
358
+ if (!delta) continue;
359
+
360
+ const chunkToolCalls = delta.tool_calls ?? [];
361
+ if (chunkToolCalls.length > 0) {
362
+ sawToolCall = true;
363
+ for (const call of chunkToolCalls) {
364
+ const toolCall = call as unknown as {
365
+ index?: number;
366
+ id?: string;
367
+ function?: { name?: string; arguments?: string };
368
+ };
369
+ const index = toolCall.index ?? 0;
370
+ const current = toolCallState[index] ?? { arguments: "" };
371
+ if (toolCall.id) current.id = toolCall.id;
372
+ if (toolCall.function?.name) current.name = toolCall.function.name;
373
+ if (toolCall.function?.arguments) current.arguments += toolCall.function.arguments;
374
+ toolCallState[index] = current;
375
+ }
376
+ }
377
+
378
+ const deltaContent = (() => {
379
+ if (typeof delta.content === "string") return delta.content;
380
+ const maybeParts = delta.content as unknown;
381
+ if (Array.isArray(maybeParts)) {
382
+ return maybeParts
383
+ .map((part) =>
384
+ typeof part === "object" &&
385
+ part !== null &&
386
+ "text" in part &&
387
+ typeof (part as Record<string, unknown>).text === "string"
388
+ ? String((part as Record<string, unknown>).text)
389
+ : ""
390
+ )
391
+ .join("");
392
+ }
393
+ return "";
394
+ })();
395
+
396
+ // Provider-dependent reasoning fields (e.g., `reasoning` or `reasoning_content`).
397
+ const deltaReasoning: string =
398
+ typeof (delta as unknown as Record<string, unknown>)?.reasoning === "string"
399
+ ? ((delta as unknown as { reasoning?: string }).reasoning as string)
400
+ : typeof (delta as unknown as Record<string, unknown>)?.reasoning_content === "string"
401
+ ? ((delta as unknown as { reasoning_content?: string }).reasoning_content as string)
402
+ : "";
403
+
404
+ // Merge reasoning + content into a single combined token stream, mirroring
405
+ // the OpenAI adapter so the UI can auto-detect <think> blocks.
406
+ let combined = "";
407
+ if (deltaReasoning && deltaReasoning.length > 0) {
408
+ if (!thinkOpen) {
409
+ combined += "<think>" + deltaReasoning;
410
+ thinkOpen = true;
411
+ } else {
412
+ combined += deltaReasoning;
413
+ }
414
+ }
415
+
416
+ if (deltaContent && deltaContent.length > 0) {
417
+ if (thinkOpen) {
418
+ combined += "</think>" + deltaContent;
419
+ thinkOpen = false;
420
+ } else {
421
+ combined += deltaContent;
422
+ }
423
+ }
424
+
425
+ if (combined.length > 0) {
426
+ lastAssistantContent += combined;
427
+ if (!sawToolCall) {
428
+ streamedContent = true;
429
+ yield { type: MessageUpdateType.Stream, token: combined };
430
+ tokenCount += combined.length;
431
+ }
432
+ }
433
+ }
434
+ logger.debug(
435
+ { sawToolCalls: Object.keys(toolCallState).length > 0, tokens: tokenCount, loop },
436
+ "[mcp] completion stream closed"
437
+ );
438
+
439
+ if (Object.keys(toolCallState).length > 0) {
440
+ // If any streamed call is missing id, perform a quick non-stream retry to recover full tool_calls with ids
441
+ const missingId = Object.values(toolCallState).some((c) => c?.name && !c?.id);
442
+ let calls: NormalizedToolCall[];
443
+ if (missingId) {
444
+ const nonStream = await openai.chat.completions.create(
445
+ { ...completionBase, messages: messagesOpenAI, stream: false },
446
+ { signal: abortSignal }
447
+ );
448
+ const tc = nonStream.choices?.[0]?.message?.tool_calls ?? [];
449
+ calls = tc.map((t) => ({
450
+ id: t.id,
451
+ name: t.function?.name ?? "",
452
+ arguments: t.function?.arguments ?? "",
453
+ }));
454
+ } else {
455
+ calls = Object.values(toolCallState)
456
+ .map((c) => (c?.id && c?.name ? c : undefined))
457
+ .filter(Boolean)
458
+ .map((c) => ({
459
+ id: c?.id ?? "",
460
+ name: c?.name ?? "",
461
+ arguments: c?.arguments ?? "",
462
+ })) as NormalizedToolCall[];
463
+ }
464
+
465
+ // Include the assistant message with tool_calls so the next round
466
+ // sees both the calls and their outputs, matching MCP branch behavior.
467
+ const toolCalls: ChatCompletionMessageToolCall[] = calls.map((call) => ({
468
+ id: call.id,
469
+ type: "function",
470
+ function: { name: call.name, arguments: call.arguments },
471
+ }));
472
+
473
+ // Avoid sending <think> content back to the model alongside tool_calls
474
+ // to prevent confusing follow-up reasoning. Strip any think blocks.
475
+ const assistantContentForToolMsg = lastAssistantContent.replace(
476
+ /<think>[\s\S]*?(?:<\/think>|$)/g,
477
+ ""
478
+ );
479
+ const assistantToolMessage: ChatCompletionMessageParam = {
480
+ role: "assistant",
481
+ content: assistantContentForToolMsg,
482
+ tool_calls: toolCalls,
483
+ };
484
+
485
+ const exec = executeToolCalls({
486
+ calls,
487
+ mapping,
488
+ servers,
489
+ parseArgs,
490
+ toPrimitive,
491
+ processToolOutput,
492
+ abortSignal,
493
+ });
494
+ let toolMsgCount = 0;
495
+ let toolRunCount = 0;
496
+ for await (const event of exec) {
497
+ if (event.type === "update") {
498
+ yield event.update;
499
+ } else {
500
+ messagesOpenAI = [
501
+ ...messagesOpenAI,
502
+ assistantToolMessage,
503
+ ...(event.summary.toolMessages ?? []),
504
+ ];
505
+ toolMsgCount = event.summary.toolMessages?.length ?? 0;
506
+ toolRunCount = event.summary.toolRuns?.length ?? 0;
507
+ logger.debug(
508
+ { toolMsgCount, toolRunCount },
509
+ "[mcp] tools executed; continuing loop for follow-up completion"
510
+ );
511
+ }
512
+ }
513
+ // Continue loop: next iteration will use tool messages to get the final content
514
+ continue;
515
+ }
516
+
517
+ // No tool calls: finalize and return
518
+ // If a <think> block is still open, close it for the final output
519
+ if (thinkOpen) {
520
+ lastAssistantContent += "</think>";
521
+ thinkOpen = false;
522
+ }
523
+ if (!streamedContent && lastAssistantContent.trim().length > 0) {
524
+ yield { type: MessageUpdateType.Stream, token: lastAssistantContent };
525
+ }
526
+ yield {
527
+ type: MessageUpdateType.FinalAnswer,
528
+ text: lastAssistantContent,
529
+ interrupted: false,
530
+ };
531
+ logger.debug({ length: lastAssistantContent.length, loop }, "[mcp] final answer emitted");
532
+ return true;
533
+ }
534
+ logger.warn("[mcp] exceeded tool-followup loops; falling back");
535
+ } catch (err) {
536
+ const msg = String(err ?? "");
537
+ const isAbort =
538
+ (abortSignal && abortSignal.aborted) ||
539
+ msg.includes("AbortError") ||
540
+ msg.includes("APIUserAbortError") ||
541
+ msg.includes("Request was aborted");
542
+ if (isAbort) {
543
+ // Expected on user stop; keep logs quiet and do not treat as error
544
+ logger.debug("[mcp] aborted by user");
545
+ return false;
546
+ }
547
+ logger.warn({ err: msg }, "[mcp] flow failed, falling back to default endpoint");
548
+ } finally {
549
+ // ensure MCP clients are closed after the turn
550
+ await drainPool();
551
+ }
552
+
553
+ return false;
554
+ }
src/lib/server/textGeneration/mcp/toolInvocation.ts ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { randomUUID } from "crypto";
2
+ import { logger } from "../../logger";
3
+ import type { MessageUpdate } from "$lib/types/MessageUpdate";
4
+ import { MessageToolUpdateType, MessageUpdateType } from "$lib/types/MessageUpdate";
5
+ import { ToolResultStatus } from "$lib/types/Tool";
6
+ import type { ChatCompletionMessageParam } from "openai/resources/chat/completions";
7
+ import type { McpToolMapping } from "$lib/server/mcp/tools";
8
+ import type { McpServerConfig } from "$lib/server/mcp/httpClient";
9
+ import { callMcpTool, type McpToolTextResponse } from "$lib/server/mcp/httpClient";
10
+ import { getClient } from "$lib/server/mcp/clientPool";
11
+ import type { Client } from "@modelcontextprotocol/sdk/client";
12
+
13
+ export type Primitive = string | number | boolean;
14
+
15
+ export type ToolRun = {
16
+ name: string;
17
+ parameters: Record<string, Primitive>;
18
+ output: string;
19
+ };
20
+
21
+ export interface NormalizedToolCall {
22
+ id: string;
23
+ name: string;
24
+ arguments: string;
25
+ }
26
+
27
+ export interface ExecuteToolCallsParams {
28
+ calls: NormalizedToolCall[];
29
+ mapping: Record<string, McpToolMapping>;
30
+ servers: McpServerConfig[];
31
+ parseArgs: (raw: unknown) => Record<string, unknown>;
32
+ toPrimitive: (value: unknown) => Primitive | undefined;
33
+ processToolOutput: (text: string) => {
34
+ annotated: string;
35
+ sources: { index: number; link: string }[];
36
+ };
37
+ abortSignal?: AbortSignal;
38
+ toolTimeoutMs?: number;
39
+ }
40
+
41
+ export interface ToolCallExecutionResult {
42
+ toolMessages: ChatCompletionMessageParam[];
43
+ toolRuns: ToolRun[];
44
+ finalAnswer?: { text: string; interrupted: boolean };
45
+ }
46
+
47
+ export type ToolExecutionEvent =
48
+ | { type: "update"; update: MessageUpdate }
49
+ | { type: "complete"; summary: ToolCallExecutionResult };
50
+
51
+ const serverMap = (servers: McpServerConfig[]): Map<string, McpServerConfig> => {
52
+ const map = new Map<string, McpServerConfig>();
53
+ for (const server of servers) {
54
+ if (server?.name) {
55
+ map.set(server.name, server);
56
+ }
57
+ }
58
+ return map;
59
+ };
60
+
61
+ export async function* executeToolCalls({
62
+ calls,
63
+ mapping,
64
+ servers,
65
+ parseArgs,
66
+ toPrimitive,
67
+ processToolOutput,
68
+ abortSignal,
69
+ toolTimeoutMs = 30_000,
70
+ }: ExecuteToolCallsParams): AsyncGenerator<ToolExecutionEvent, void, undefined> {
71
+ const toolMessages: ChatCompletionMessageParam[] = [];
72
+ const toolRuns: ToolRun[] = [];
73
+ const serverLookup = serverMap(servers);
74
+ // Pre-emit call + ETA updates and prepare tasks
75
+ type TaskResult = {
76
+ index: number;
77
+ output?: string;
78
+ structured?: unknown;
79
+ blocks?: unknown[];
80
+ error?: string;
81
+ uuid: string;
82
+ paramsClean: Record<string, Primitive>;
83
+ };
84
+
85
+ const prepared = calls.map((call) => {
86
+ const argsObj = parseArgs(call.arguments);
87
+ const paramsClean: Record<string, Primitive> = {};
88
+ for (const [k, v] of Object.entries(argsObj ?? {})) {
89
+ const prim = toPrimitive(v);
90
+ if (prim !== undefined) paramsClean[k] = prim;
91
+ }
92
+ return { call, argsObj, paramsClean, uuid: randomUUID() };
93
+ });
94
+
95
+ for (const p of prepared) {
96
+ yield {
97
+ type: "update",
98
+ update: {
99
+ type: MessageUpdateType.Tool,
100
+ subtype: MessageToolUpdateType.Call,
101
+ uuid: p.uuid,
102
+ call: { name: p.call.name, parameters: p.paramsClean },
103
+ },
104
+ };
105
+ yield {
106
+ type: "update",
107
+ update: {
108
+ type: MessageUpdateType.Tool,
109
+ subtype: MessageToolUpdateType.ETA,
110
+ uuid: p.uuid,
111
+ eta: 10,
112
+ },
113
+ };
114
+ }
115
+
116
+ // Preload clients per distinct server used in this batch
117
+ const distinctServerNames = Array.from(
118
+ new Set(prepared.map((p) => mapping[p.call.name]?.server).filter(Boolean) as string[])
119
+ );
120
+ const clientMap = new Map<string, Client>();
121
+ await Promise.all(
122
+ distinctServerNames.map(async (name) => {
123
+ const cfg = serverLookup.get(name);
124
+ if (!cfg) return;
125
+ try {
126
+ const client = await getClient(cfg, abortSignal);
127
+ clientMap.set(name, client);
128
+ } catch (e) {
129
+ logger.warn({ server: name, err: String(e) }, "[mcp] failed to connect client");
130
+ }
131
+ })
132
+ );
133
+
134
+ // Async queue to stream results in finish order
135
+ function createQueue<T>() {
136
+ const items: T[] = [];
137
+ const waiters: Array<(v: IteratorResult<T>) => void> = [];
138
+ let closed = false;
139
+ return {
140
+ push(item: T) {
141
+ const waiter = waiters.shift();
142
+ if (waiter) waiter({ value: item, done: false });
143
+ else items.push(item);
144
+ },
145
+ close() {
146
+ closed = true;
147
+ let waiter: ((v: IteratorResult<T>) => void) | undefined;
148
+ while ((waiter = waiters.shift())) {
149
+ waiter({ value: undefined as unknown as T, done: true });
150
+ }
151
+ },
152
+ async *iterator() {
153
+ for (;;) {
154
+ if (items.length) {
155
+ const first = items.shift();
156
+ if (first !== undefined) yield first as T;
157
+ continue;
158
+ }
159
+ if (closed) return;
160
+ const value: IteratorResult<T> = await new Promise((res) => waiters.push(res));
161
+ if (value.done) return;
162
+ yield value.value as T;
163
+ }
164
+ },
165
+ };
166
+ }
167
+
168
+ const q = createQueue<TaskResult>();
169
+
170
+ const tasks = prepared.map(async (p, index) => {
171
+ const mappingEntry = mapping[p.call.name];
172
+ if (!mappingEntry) {
173
+ q.push({
174
+ index,
175
+ error: `Unknown MCP function: ${p.call.name}`,
176
+ uuid: p.uuid,
177
+ paramsClean: p.paramsClean,
178
+ });
179
+ return;
180
+ }
181
+ const serverCfg = serverLookup.get(mappingEntry.server);
182
+ if (!serverCfg) {
183
+ q.push({
184
+ index,
185
+ error: `Unknown MCP server: ${mappingEntry.server}`,
186
+ uuid: p.uuid,
187
+ paramsClean: p.paramsClean,
188
+ });
189
+ return;
190
+ }
191
+ const client = clientMap.get(mappingEntry.server);
192
+ try {
193
+ logger.debug(
194
+ { server: mappingEntry.server, tool: mappingEntry.tool, parameters: p.paramsClean },
195
+ "[mcp] invoking tool"
196
+ );
197
+ const toolResponse: McpToolTextResponse = await callMcpTool(
198
+ serverCfg,
199
+ mappingEntry.tool,
200
+ p.argsObj,
201
+ {
202
+ client,
203
+ signal: abortSignal,
204
+ timeoutMs: toolTimeoutMs,
205
+ }
206
+ );
207
+ const { annotated } = processToolOutput(toolResponse.text ?? "");
208
+ logger.debug(
209
+ { server: mappingEntry.server, tool: mappingEntry.tool },
210
+ "[mcp] tool call completed"
211
+ );
212
+ q.push({
213
+ index,
214
+ output: annotated,
215
+ structured: toolResponse.structured,
216
+ blocks: toolResponse.content,
217
+ uuid: p.uuid,
218
+ paramsClean: p.paramsClean,
219
+ });
220
+ } catch (err) {
221
+ const message = err instanceof Error ? err.message : String(err);
222
+ logger.warn(
223
+ { server: mappingEntry.server, tool: mappingEntry.tool, err: message },
224
+ "[mcp] tool call failed"
225
+ );
226
+ q.push({ index, error: message, uuid: p.uuid, paramsClean: p.paramsClean });
227
+ }
228
+ });
229
+
230
+ // kick off and stream as they finish
231
+ Promise.allSettled(tasks).then(() => q.close());
232
+
233
+ const results: TaskResult[] = [];
234
+ for await (const r of q.iterator()) {
235
+ results.push(r);
236
+ if (r.error) {
237
+ yield {
238
+ type: "update",
239
+ update: {
240
+ type: MessageUpdateType.Tool,
241
+ subtype: MessageToolUpdateType.Error,
242
+ uuid: r.uuid,
243
+ message: r.error,
244
+ },
245
+ };
246
+ } else {
247
+ yield {
248
+ type: "update",
249
+ update: {
250
+ type: MessageUpdateType.Tool,
251
+ subtype: MessageToolUpdateType.Result,
252
+ uuid: r.uuid,
253
+ result: {
254
+ status: ToolResultStatus.Success,
255
+ call: { name: prepared[r.index].call.name, parameters: r.paramsClean },
256
+ outputs: [
257
+ {
258
+ text: r.output ?? "",
259
+ structured: r.structured,
260
+ content: r.blocks,
261
+ } as unknown as Record<string, unknown>,
262
+ ],
263
+ display: true,
264
+ },
265
+ },
266
+ };
267
+ }
268
+ }
269
+
270
+ // Collate outputs in original call order
271
+ results.sort((a, b) => a.index - b.index);
272
+ for (const r of results) {
273
+ const name = prepared[r.index].call.name;
274
+ const id = prepared[r.index].call.id;
275
+ if (!r.error) {
276
+ const output = r.output ?? "";
277
+ toolRuns.push({ name, parameters: r.paramsClean, output });
278
+ // For the LLM follow-up call, we keep only the textual output
279
+ toolMessages.push({ role: "tool", tool_call_id: id, content: output });
280
+ }
281
+ }
282
+
283
+ yield { type: "complete", summary: { toolMessages, toolRuns } };
284
+ }
src/lib/server/textGeneration/reasoning.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { generateFromDefaultEndpoint } from "$lib/server/generateFromDefaultEndpoint";
2
+ import { MessageUpdateType } from "$lib/types/MessageUpdate";
3
+
4
+ export async function generateSummaryOfReasoning(
5
+ reasoning: string,
6
+ modelId: string | undefined,
7
+ locals: App.Locals | undefined
8
+ ): Promise<string> {
9
+ const prompt = `Summarize concisely the following reasoning for the user. Keep it short (one short paragraph).\n\n${reasoning}`;
10
+ const summary = await (async () => {
11
+ const it = generateFromDefaultEndpoint({
12
+ messages: [{ from: "user", content: prompt }],
13
+ modelId,
14
+ locals,
15
+ });
16
+ let out = "";
17
+ for await (const update of it) {
18
+ if (update.type === MessageUpdateType.Stream) out += update.token;
19
+ }
20
+ return out;
21
+ })();
22
+ return summary.trim();
23
+ }
src/lib/server/textGeneration/types.ts CHANGED
@@ -15,6 +15,8 @@ export interface TextGenerationContext {
15
  username?: string;
16
  /** Force-enable multimodal handling for endpoints that support it */
17
  forceMultimodal?: boolean;
 
 
18
  locals: App.Locals | undefined;
19
  abortController: AbortController;
20
  }
 
15
  username?: string;
16
  /** Force-enable multimodal handling for endpoints that support it */
17
  forceMultimodal?: boolean;
18
+ /** Force-enable tool calling even if model does not advertise support */
19
+ forceTools?: boolean;
20
  locals: App.Locals | undefined;
21
  abortController: AbortController;
22
  }
src/lib/server/textGeneration/utils/routing.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { EndpointMessage } from "../../endpoints/endpoints";
2
+
3
+ const ROUTER_REASONING_REGEX = /<think>[\s\S]*?(?:<\/think>|$)/g;
4
+
5
+ export function stripReasoningBlocks(text: string): string {
6
+ const stripped = text.replace(ROUTER_REASONING_REGEX, "");
7
+ return stripped === text ? text : stripped.trim();
8
+ }
9
+
10
+ export function stripReasoningFromMessageForRouting(message: EndpointMessage): EndpointMessage {
11
+ const clone = { ...message } as EndpointMessage & { reasoning?: string };
12
+ if ("reasoning" in clone) {
13
+ delete clone.reasoning;
14
+ }
15
+ const content =
16
+ typeof message.content === "string" ? stripReasoningBlocks(message.content) : message.content;
17
+ return {
18
+ ...clone,
19
+ content,
20
+ };
21
+ }
src/lib/server/textGeneration/utils/toolPrompt.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { OpenAiTool } from "$lib/server/mcp/tools";
2
+
3
+ export function buildToolPreprompt(tools: OpenAiTool[]): string {
4
+ if (!Array.isArray(tools) || tools.length === 0) return "";
5
+ const names = tools
6
+ .map((t) => (t?.function?.name ? String(t.function.name) : ""))
7
+ .filter((s) => s.length > 0);
8
+ if (names.length === 0) return "";
9
+ const currentDate = new Date().toLocaleDateString("en-US", {
10
+ year: "numeric",
11
+ month: "long",
12
+ day: "numeric",
13
+ });
14
+ return `You can use the following tools if helpful: ${names.join(", ")}. Today's date: ${currentDate}. If a tool generates an image, you can inline it directly: ![alt text](image_url).`;
15
+ }
src/lib/server/urlSafety.ts ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Shared server-side URL safety helper (exact behavior preserved)
2
+ export function isValidUrl(urlString: string): boolean {
3
+ try {
4
+ const url = new URL(urlString);
5
+ // Only allow HTTPS protocol
6
+ if (url.protocol !== "https:") {
7
+ return false;
8
+ }
9
+ // Prevent localhost/private IPs (basic check)
10
+ const hostname = url.hostname.toLowerCase();
11
+ if (
12
+ hostname === "localhost" ||
13
+ hostname.startsWith("127.") ||
14
+ hostname.startsWith("192.168.") ||
15
+ hostname.startsWith("172.16.") ||
16
+ hostname === "[::1]" ||
17
+ hostname === "0.0.0.0"
18
+ ) {
19
+ return false;
20
+ }
21
+ return true;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
src/lib/stores/mcpServers.ts ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * MCP Servers Store
3
+ * Manages base (env-configured) and custom (user-added) MCP servers
4
+ * Stores custom servers and selection state in browser localStorage
5
+ */
6
+
7
+ import { writable, derived } from "svelte/store";
8
+ import { base } from "$app/paths";
9
+ import { browser } from "$app/environment";
10
+ import type { MCPServer, ServerStatus, MCPTool } from "$lib/types/Tool";
11
+
12
+ const STORAGE_KEYS = {
13
+ CUSTOM_SERVERS: "mcp:custom-servers",
14
+ SELECTED_IDS: "mcp:selected-ids",
15
+ } as const;
16
+
17
+ // Load custom servers from localStorage
18
+ function loadCustomServers(): MCPServer[] {
19
+ if (!browser) return [];
20
+
21
+ try {
22
+ const json = localStorage.getItem(STORAGE_KEYS.CUSTOM_SERVERS);
23
+ return json ? JSON.parse(json) : [];
24
+ } catch (error) {
25
+ console.error("Failed to load custom MCP servers from localStorage:", error);
26
+ return [];
27
+ }
28
+ }
29
+
30
+ // Load selected server IDs from localStorage
31
+ function loadSelectedIds(): Set<string> {
32
+ if (!browser) return new Set();
33
+
34
+ try {
35
+ const json = localStorage.getItem(STORAGE_KEYS.SELECTED_IDS);
36
+ const ids: string[] = json ? JSON.parse(json) : [];
37
+ return new Set(ids);
38
+ } catch (error) {
39
+ console.error("Failed to load selected MCP server IDs from localStorage:", error);
40
+ return new Set();
41
+ }
42
+ }
43
+
44
+ // Save custom servers to localStorage
45
+ function saveCustomServers(servers: MCPServer[]) {
46
+ if (!browser) return;
47
+
48
+ try {
49
+ localStorage.setItem(STORAGE_KEYS.CUSTOM_SERVERS, JSON.stringify(servers));
50
+ } catch (error) {
51
+ console.error("Failed to save custom MCP servers to localStorage:", error);
52
+ }
53
+ }
54
+
55
+ // Save selected IDs to localStorage
56
+ function saveSelectedIds(ids: Set<string>) {
57
+ if (!browser) return;
58
+
59
+ try {
60
+ localStorage.setItem(STORAGE_KEYS.SELECTED_IDS, JSON.stringify([...ids]));
61
+ } catch (error) {
62
+ console.error("Failed to save selected MCP server IDs to localStorage:", error);
63
+ }
64
+ }
65
+
66
+ // Store for all servers (base + custom)
67
+ export const allMcpServers = writable<MCPServer[]>([]);
68
+
69
+ // Store for selected server IDs
70
+ export const selectedServerIds = writable<Set<string>>(loadSelectedIds());
71
+
72
+ // Auto-persist selected IDs when they change
73
+ if (browser) {
74
+ selectedServerIds.subscribe((ids) => {
75
+ saveSelectedIds(ids);
76
+ });
77
+ }
78
+
79
+ // Derived store: only enabled servers
80
+ export const enabledServers = derived([allMcpServers, selectedServerIds], ([$all, $selected]) =>
81
+ $all.filter((s) => $selected.has(s.id))
82
+ );
83
+
84
+ // Derived store: count of enabled servers
85
+ export const enabledServersCount = derived(enabledServers, ($enabled) => $enabled.length);
86
+
87
+ // Note: Authorization overlay (with user's HF token) for the Hugging Face MCP host
88
+ // is applied server-side when enabled via MCP_FORWARD_HF_USER_TOKEN.
89
+
90
+ /**
91
+ * Refresh base servers from API and merge with custom servers
92
+ */
93
+ export async function refreshMcpServers() {
94
+ try {
95
+ const response = await fetch(`${base}/api/mcp/servers`);
96
+ if (!response.ok) {
97
+ throw new Error(`Failed to fetch base servers: ${response.statusText}`);
98
+ }
99
+
100
+ const baseServers: MCPServer[] = await response.json();
101
+ const customServers = loadCustomServers();
102
+
103
+ // Merge base and custom servers
104
+ const merged = [...baseServers, ...customServers];
105
+ allMcpServers.set(merged);
106
+
107
+ // Prune selected IDs that no longer correspond to existing servers
108
+ const validIds = new Set(merged.map((s) => s.id));
109
+ selectedServerIds.update(($ids) => {
110
+ const filtered = new Set([...$ids].filter((id) => validIds.has(id)));
111
+ return filtered;
112
+ });
113
+ } catch (error) {
114
+ console.error("Failed to refresh MCP servers:", error);
115
+ // On error, just use custom servers
116
+ allMcpServers.set(loadCustomServers());
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Toggle a server on/off
122
+ */
123
+ export function toggleServer(id: string) {
124
+ selectedServerIds.update(($ids) => {
125
+ const newSet = new Set($ids);
126
+ if (newSet.has(id)) {
127
+ newSet.delete(id);
128
+ } else {
129
+ newSet.add(id);
130
+ }
131
+ return newSet;
132
+ });
133
+ }
134
+
135
+ /**
136
+ * Add a custom MCP server
137
+ */
138
+ export function addCustomServer(server: Omit<MCPServer, "id" | "type" | "status">): string {
139
+ const newServer: MCPServer = {
140
+ ...server,
141
+ id: crypto.randomUUID(),
142
+ type: "custom",
143
+ status: "disconnected",
144
+ };
145
+
146
+ const customServers = loadCustomServers();
147
+ customServers.push(newServer);
148
+ saveCustomServers(customServers);
149
+
150
+ // Refresh all servers to include the new one
151
+ refreshMcpServers();
152
+
153
+ return newServer.id;
154
+ }
155
+
156
+ /**
157
+ * Update an existing custom server
158
+ */
159
+ export function updateCustomServer(id: string, updates: Partial<MCPServer>) {
160
+ const customServers = loadCustomServers();
161
+ const index = customServers.findIndex((s) => s.id === id);
162
+
163
+ if (index !== -1) {
164
+ customServers[index] = { ...customServers[index], ...updates };
165
+ saveCustomServers(customServers);
166
+ refreshMcpServers();
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Delete a custom server
172
+ */
173
+ export function deleteCustomServer(id: string) {
174
+ const customServers = loadCustomServers();
175
+ const filtered = customServers.filter((s) => s.id !== id);
176
+ saveCustomServers(filtered);
177
+
178
+ // Also remove from selected IDs
179
+ selectedServerIds.update(($ids) => {
180
+ const newSet = new Set($ids);
181
+ newSet.delete(id);
182
+ return newSet;
183
+ });
184
+
185
+ refreshMcpServers();
186
+ }
187
+
188
+ /**
189
+ * Update server status (from health check)
190
+ */
191
+ export function updateServerStatus(
192
+ id: string,
193
+ status: ServerStatus,
194
+ errorMessage?: string,
195
+ tools?: MCPTool[],
196
+ authRequired?: boolean
197
+ ) {
198
+ allMcpServers.update(($servers) =>
199
+ $servers.map((s) =>
200
+ s.id === id
201
+ ? {
202
+ ...s,
203
+ status,
204
+ errorMessage,
205
+ tools,
206
+ authRequired,
207
+ }
208
+ : s
209
+ )
210
+ );
211
+ }
212
+
213
+ /**
214
+ * Run health check on a server
215
+ */
216
+ export async function healthCheckServer(
217
+ server: MCPServer
218
+ ): Promise<{ ready: boolean; tools?: MCPTool[]; error?: string }> {
219
+ try {
220
+ updateServerStatus(server.id, "connecting");
221
+
222
+ const response = await fetch(`${base}/api/mcp/health`, {
223
+ method: "POST",
224
+ headers: { "Content-Type": "application/json" },
225
+ body: JSON.stringify({ url: server.url, headers: server.headers }),
226
+ });
227
+
228
+ const result = await response.json();
229
+
230
+ if (result.ready && result.tools) {
231
+ updateServerStatus(server.id, "connected", undefined, result.tools, false);
232
+ return { ready: true, tools: result.tools };
233
+ } else {
234
+ updateServerStatus(server.id, "error", result.error, undefined, Boolean(result.authRequired));
235
+ return { ready: false, error: result.error };
236
+ }
237
+ } catch (error) {
238
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
239
+ updateServerStatus(server.id, "error", errorMessage);
240
+ return { ready: false, error: errorMessage };
241
+ }
242
+ }
243
+
244
+ // Initialize on module load
245
+ if (browser) {
246
+ refreshMcpServers();
247
+ }
src/lib/stores/settings.ts CHANGED
@@ -12,6 +12,7 @@ type SettingsStore = {
12
  activeModel: string;
13
  customPrompts: Record<string, string>;
14
  multimodalOverrides: Record<string, boolean>;
 
15
  recentlySaved: boolean;
16
  disableStream: boolean;
17
  directPaste: boolean;
 
12
  activeModel: string;
13
  customPrompts: Record<string, string>;
14
  multimodalOverrides: Record<string, boolean>;
15
+ toolsOverrides: Record<string, boolean>;
16
  recentlySaved: boolean;
17
  disableStream: boolean;
18
  directPaste: boolean;
src/lib/types/Message.ts CHANGED
@@ -9,6 +9,8 @@ export type Message = Partial<Timestamps> & {
9
  content: string;
10
  updates?: MessageUpdate[];
11
 
 
 
12
  score?: -1 | 0 | 1;
13
  /**
14
  * Either contains the base64 encoded image data
 
9
  content: string;
10
  updates?: MessageUpdate[];
11
 
12
+ // Optional server or client-side reasoning content (<think> blocks)
13
+ reasoning?: string;
14
  score?: -1 | 0 | 1;
15
  /**
16
  * Either contains the base64 encoded image data
src/lib/types/MessageUpdate.ts CHANGED
@@ -1,8 +1,10 @@
1
  import type { InferenceProvider } from "@huggingface/inference";
 
2
 
3
  export type MessageUpdate =
4
  | MessageStatusUpdate
5
  | MessageTitleUpdate
 
6
  | MessageStreamUpdate
7
  | MessageFileUpdate
8
  | MessageFinalAnswerUpdate
@@ -12,6 +14,7 @@ export type MessageUpdate =
12
  export enum MessageUpdateType {
13
  Status = "status",
14
  Title = "title",
 
15
  Stream = "stream",
16
  File = "file",
17
  FinalAnswer = "finalAnswer",
@@ -43,6 +46,43 @@ export interface MessageStreamUpdate {
43
  token: string;
44
  }
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  export enum MessageReasoningUpdateType {
47
  Stream = "stream",
48
  Status = "status",
 
1
  import type { InferenceProvider } from "@huggingface/inference";
2
+ import type { ToolCall, ToolResult } from "$lib/types/Tool";
3
 
4
  export type MessageUpdate =
5
  | MessageStatusUpdate
6
  | MessageTitleUpdate
7
+ | MessageToolUpdate
8
  | MessageStreamUpdate
9
  | MessageFileUpdate
10
  | MessageFinalAnswerUpdate
 
14
  export enum MessageUpdateType {
15
  Status = "status",
16
  Title = "title",
17
+ Tool = "tool",
18
  Stream = "stream",
19
  File = "file",
20
  FinalAnswer = "finalAnswer",
 
46
  token: string;
47
  }
48
 
49
+ // Tool updates (for MCP and function calling)
50
+ export enum MessageToolUpdateType {
51
+ Call = "call",
52
+ Result = "result",
53
+ Error = "error",
54
+ ETA = "eta",
55
+ }
56
+
57
+ interface MessageToolUpdateBase<TSubtype extends MessageToolUpdateType> {
58
+ type: MessageUpdateType.Tool;
59
+ subtype: TSubtype;
60
+ uuid: string;
61
+ }
62
+
63
+ export interface MessageToolCallUpdate extends MessageToolUpdateBase<MessageToolUpdateType.Call> {
64
+ call: ToolCall;
65
+ }
66
+
67
+ export interface MessageToolResultUpdate
68
+ extends MessageToolUpdateBase<MessageToolUpdateType.Result> {
69
+ result: ToolResult;
70
+ }
71
+
72
+ export interface MessageToolErrorUpdate extends MessageToolUpdateBase<MessageToolUpdateType.Error> {
73
+ message: string;
74
+ }
75
+
76
+ export interface MessageToolEtaUpdate extends MessageToolUpdateBase<MessageToolUpdateType.ETA> {
77
+ eta: number;
78
+ }
79
+
80
+ export type MessageToolUpdate =
81
+ | MessageToolCallUpdate
82
+ | MessageToolResultUpdate
83
+ | MessageToolErrorUpdate
84
+ | MessageToolEtaUpdate;
85
+
86
  export enum MessageReasoningUpdateType {
87
  Stream = "stream",
88
  Status = "status",
src/lib/types/Settings.ts CHANGED
@@ -21,6 +21,12 @@ export interface Settings extends Timestamps {
21
  */
22
  multimodalOverrides?: Record<string, boolean>;
23
 
 
 
 
 
 
 
24
  /**
25
  * Per-model toggle to hide Omni prompt suggestions shown near the composer.
26
  * When set to `true`, prompt examples for that model are suppressed.
@@ -38,6 +44,7 @@ export const DEFAULT_SETTINGS = {
38
  activeModel: defaultModel.id,
39
  customPrompts: {},
40
  multimodalOverrides: {},
 
41
  hidePromptExamples: {},
42
  disableStream: false,
43
  directPaste: false,
 
21
  */
22
  multimodalOverrides?: Record<string, boolean>;
23
 
24
+ /**
25
+ * Per‑model overrides to enable tool calling (OpenAI tools/function calling)
26
+ * even when not advertised by the provider list. Only `true` is meaningful.
27
+ */
28
+ toolsOverrides?: Record<string, boolean>;
29
+
30
  /**
31
  * Per-model toggle to hide Omni prompt suggestions shown near the composer.
32
  * When set to `true`, prompt examples for that model are suppressed.
 
44
  activeModel: defaultModel.id,
45
  customPrompts: {},
46
  multimodalOverrides: {},
47
+ toolsOverrides: {},
48
  hidePromptExamples: {},
49
  disableStream: false,
50
  directPaste: false,
src/lib/types/Tool.ts ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export enum ToolResultStatus {
2
+ Success = "success",
3
+ Error = "error",
4
+ }
5
+
6
+ export interface ToolCall {
7
+ name: string;
8
+ parameters: Record<string, string | number | boolean>;
9
+ toolId?: string;
10
+ }
11
+
12
+ export interface ToolResultSuccess {
13
+ status: ToolResultStatus.Success;
14
+ call: ToolCall;
15
+ outputs: Record<string, unknown>[];
16
+ display?: boolean;
17
+ }
18
+
19
+ export interface ToolResultError {
20
+ status: ToolResultStatus.Error;
21
+ call: ToolCall;
22
+ message: string;
23
+ display?: boolean;
24
+ }
25
+
26
+ export type ToolResult = ToolResultSuccess | ToolResultError;
27
+
28
+ export interface ToolFront {
29
+ _id: string;
30
+ name: string;
31
+ displayName?: string;
32
+ description?: string;
33
+ color?: string;
34
+ icon?: string;
35
+ type?: "config" | "community";
36
+ isOnByDefault?: boolean;
37
+ isLocked?: boolean;
38
+ mimeTypes?: string[];
39
+ timeToUseMS?: number;
40
+ }
41
+
42
+ // MCP Server types
43
+ export interface KeyValuePair {
44
+ key: string;
45
+ value: string;
46
+ }
47
+
48
+ export type ServerStatus = "connected" | "connecting" | "disconnected" | "error";
49
+
50
+ export interface MCPTool {
51
+ name: string;
52
+ description?: string;
53
+ inputSchema?: unknown;
54
+ }
55
+
56
+ export interface MCPServer {
57
+ id: string;
58
+ name: string;
59
+ url: string;
60
+ type: "base" | "custom";
61
+ headers?: KeyValuePair[];
62
+ env?: KeyValuePair[];
63
+ status?: ServerStatus;
64
+ isLocked?: boolean;
65
+ tools?: MCPTool[];
66
+ errorMessage?: string;
67
+ // Indicates server reports or appears to require OAuth or other auth
68
+ authRequired?: boolean;
69
+ }
70
+
71
+ export interface MCPServerApi {
72
+ url: string;
73
+ headers?: KeyValuePair[];
74
+ }