mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
📦 chore: Bump MCP SDK: Fix Types and MCP OAuth due to Update (#10811)
* chore: Bump @modelcontextprotocol/sdk to version 1.24.3 * refactor: Update resource handling in MCP parsers and types - Simplified resource text checks in `parseAsString` and `formatToolContent` functions to ensure proper existence checks. - Removed unnecessary resource name and description handling to streamline output. - Updated type definitions in `index.ts` to align with the new structure from `@modelcontextprotocol/sdk`, enhancing type safety and clarity. - Added `logo_uri` and `tos_uri` properties to `MCPOAuthHandler` for improved OAuth metadata support. * refactor: Update custom endpoint configurations and type definitions - Removed unused type imports and streamlined the custom parameters handling in `loadCustomEndpointsConfig`. - Adjusted the `TCustomEndpointsConfig` type to utilize `TConfig` instead of `TEndpoint`, enhancing type accuracy. - Made the endpoint schema optional in the configuration to improve flexibility. * fix: Implement token cleanup and error handling for invalid OAuth tokens - Added `cleanupInvalidTokens` method to remove invalid OAuth tokens from storage when detected. - Introduced `isInvalidTokenError` method to identify errors indicating revoked or expired tokens. - Integrated token cleanup logic into the connection attempt process to ensure fresh OAuth flow on invalid token detection. * feat: Add revoke OAuth functionality in Server Initialization - Introduced a new button to revoke OAuth for servers, enhancing user control over OAuth permissions. - Updated the `useMCPServerManager` hook to include a standalone `revokeOAuthForServer` function for managing OAuth revocation. - Adjusted the UI to conditionally render the revoke button based on server requirements. * fix: error handling for authentication in MCPConnection - Updated the error handling logic in MCPConnection to better identify various authentication error indicators, including 401 status, invalid tokens, and unauthorized messages. - Removed the deprecated cleanupInvalidTokens method and integrated its logic into the connection attempt process for improved clarity and efficiency. - Adjusted the MCPConnectionFactory to streamline the connection attempt process and handle OAuth errors more effectively. * refactor: Update button rendering in ServerInitializationSection - Removed the existing button for server initialization and replaced it with a new button implementation, maintaining the same functionality. - Ensured consistent rendering of the button within the component's layout. * chore: update resource type usage in parsers.test.ts
This commit is contained in:
parent
e6288c379c
commit
394bb6242b
14 changed files with 155 additions and 90 deletions
|
|
@ -47,7 +47,7 @@
|
||||||
"@librechat/api": "*",
|
"@librechat/api": "*",
|
||||||
"@librechat/data-schemas": "*",
|
"@librechat/data-schemas": "*",
|
||||||
"@microsoft/microsoft-graph-client": "^3.0.7",
|
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||||
"@modelcontextprotocol/sdk": "^1.21.0",
|
"@modelcontextprotocol/sdk": "^1.24.3",
|
||||||
"@node-saml/passport-saml": "^5.1.0",
|
"@node-saml/passport-saml": "^5.1.0",
|
||||||
"@smithy/node-http-handler": "^4.4.5",
|
"@smithy/node-http-handler": "^4.4.5",
|
||||||
"axios": "^1.12.1",
|
"axios": "^1.12.1",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RefreshCw } from 'lucide-react';
|
import { RefreshCw, Trash2 } from 'lucide-react';
|
||||||
import { Button, Spinner } from '@librechat/client';
|
import { Button, Spinner } from '@librechat/client';
|
||||||
import { useLocalize, useMCPServerManager, useMCPConnectionStatus } from '~/hooks';
|
import { useLocalize, useMCPServerManager, useMCPConnectionStatus } from '~/hooks';
|
||||||
import { useGetStartupConfig } from '~/data-provider';
|
|
||||||
|
|
||||||
interface ServerInitializationSectionProps {
|
interface ServerInitializationSectionProps {
|
||||||
sidePanel?: boolean;
|
sidePanel?: boolean;
|
||||||
|
|
@ -22,12 +21,13 @@ export default function ServerInitializationSection({
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
getOAuthUrl,
|
||||||
|
isCancellable,
|
||||||
|
isInitializing,
|
||||||
|
cancelOAuthFlow,
|
||||||
initializeServer,
|
initializeServer,
|
||||||
availableMCPServers,
|
availableMCPServers,
|
||||||
cancelOAuthFlow,
|
revokeOAuthForServer,
|
||||||
isInitializing,
|
|
||||||
isCancellable,
|
|
||||||
getOAuthUrl,
|
|
||||||
} = useMCPServerManager({ conversationId });
|
} = useMCPServerManager({ conversationId });
|
||||||
|
|
||||||
const { connectionStatus } = useMCPConnectionStatus({
|
const { connectionStatus } = useMCPConnectionStatus({
|
||||||
|
|
@ -73,7 +73,6 @@ export default function ServerInitializationSection({
|
||||||
|
|
||||||
// Unified button rendering
|
// Unified button rendering
|
||||||
const isReinit = shouldShowReinit;
|
const isReinit = shouldShowReinit;
|
||||||
const outerClass = isReinit ? 'flex justify-start' : 'flex justify-end';
|
|
||||||
const buttonVariant = isReinit ? undefined : 'default';
|
const buttonVariant = isReinit ? undefined : 'default';
|
||||||
|
|
||||||
let buttonText = '';
|
let buttonText = '';
|
||||||
|
|
@ -94,13 +93,24 @@ export default function ServerInitializationSection({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={outerClass}>
|
<div className="flex items-center gap-2">
|
||||||
|
{requiresOAuth && revokeOAuthForServer && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => revokeOAuthForServer(serverName)}
|
||||||
|
aria-label={localize('com_ui_revoke')}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
{localize('com_ui_revoke')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant={buttonVariant}
|
variant={buttonVariant}
|
||||||
onClick={() => initializeServer(serverName, false)}
|
onClick={() => initializeServer(serverName, false)}
|
||||||
disabled={isServerInitializing}
|
disabled={isServerInitializing}
|
||||||
size={sidePanel ? 'sm' : 'default'}
|
size={sidePanel ? 'sm' : 'default'}
|
||||||
className="w-full"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
{buttonText}
|
{buttonText}
|
||||||
|
|
|
||||||
|
|
@ -517,6 +517,19 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
||||||
[selectedToolForConfig, updateUserPluginsMutation, mcpValues, setMCPValues],
|
[selectedToolForConfig, updateUserPluginsMutation, mcpValues, setMCPValues],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** Standalone revoke function for OAuth servers - doesn't require selectedToolForConfig */
|
||||||
|
const revokeOAuthForServer = useCallback(
|
||||||
|
(serverName: string) => {
|
||||||
|
const payload: TUpdateUserPlugins = {
|
||||||
|
pluginKey: `${Constants.mcp_prefix}${serverName}`,
|
||||||
|
action: 'uninstall',
|
||||||
|
auth: {},
|
||||||
|
};
|
||||||
|
updateUserPluginsMutation.mutate(payload);
|
||||||
|
},
|
||||||
|
[updateUserPluginsMutation],
|
||||||
|
);
|
||||||
|
|
||||||
const handleSave = useCallback(
|
const handleSave = useCallback(
|
||||||
(authData: Record<string, string>) => {
|
(authData: Record<string, string>) => {
|
||||||
if (selectedToolForConfig) {
|
if (selectedToolForConfig) {
|
||||||
|
|
@ -678,6 +691,7 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
||||||
setSelectedToolForConfig,
|
setSelectedToolForConfig,
|
||||||
handleSave,
|
handleSave,
|
||||||
handleRevoke,
|
handleRevoke,
|
||||||
|
revokeOAuthForServer,
|
||||||
getServerStatusIconProps,
|
getServerStatusIconProps,
|
||||||
getConfigDialogProps,
|
getConfigDialogProps,
|
||||||
checkEffectivePermission,
|
checkEffectivePermission,
|
||||||
|
|
|
||||||
45
package-lock.json
generated
45
package-lock.json
generated
|
|
@ -61,7 +61,7 @@
|
||||||
"@librechat/api": "*",
|
"@librechat/api": "*",
|
||||||
"@librechat/data-schemas": "*",
|
"@librechat/data-schemas": "*",
|
||||||
"@microsoft/microsoft-graph-client": "^3.0.7",
|
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||||
"@modelcontextprotocol/sdk": "^1.21.0",
|
"@modelcontextprotocol/sdk": "^1.24.3",
|
||||||
"@node-saml/passport-saml": "^5.1.0",
|
"@node-saml/passport-saml": "^5.1.0",
|
||||||
"@smithy/node-http-handler": "^4.4.5",
|
"@smithy/node-http-handler": "^4.4.5",
|
||||||
"axios": "^1.12.1",
|
"axios": "^1.12.1",
|
||||||
|
|
@ -1467,9 +1467,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"api/node_modules/jose": {
|
"api/node_modules/jose": {
|
||||||
"version": "6.0.11",
|
"version": "6.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
|
||||||
"integrity": "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==",
|
"integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/panva"
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
|
@ -18362,9 +18362,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@modelcontextprotocol/sdk": {
|
"node_modules/@modelcontextprotocol/sdk": {
|
||||||
"version": "1.21.0",
|
"version": "1.24.3",
|
||||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.24.3.tgz",
|
||||||
"integrity": "sha512-YFBsXJMFCyI1zP98u7gezMFKX4lgu/XpoZJk7ufI6UlFKXLj2hAMUuRlQX/nrmIPOmhRrG6tw2OQ2ZA/ZlXYpQ==",
|
"integrity": "sha512-YgSHW29fuzKKAHTGe9zjNoo+yF8KaQPzDC2W9Pv41E7/57IfY+AMGJ/aDFlgTLcVVELoggKE4syABCE75u3NCw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^8.17.1",
|
"ajv": "^8.17.1",
|
||||||
|
|
@ -18376,23 +18376,37 @@
|
||||||
"eventsource-parser": "^3.0.0",
|
"eventsource-parser": "^3.0.0",
|
||||||
"express": "^5.0.1",
|
"express": "^5.0.1",
|
||||||
"express-rate-limit": "^7.5.0",
|
"express-rate-limit": "^7.5.0",
|
||||||
|
"jose": "^6.1.1",
|
||||||
"pkce-challenge": "^5.0.0",
|
"pkce-challenge": "^5.0.0",
|
||||||
"raw-body": "^3.0.0",
|
"raw-body": "^3.0.0",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.25 || ^4.0",
|
||||||
"zod-to-json-schema": "^3.24.1"
|
"zod-to-json-schema": "^3.25.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@cfworker/json-schema": "^4.1.1"
|
"@cfworker/json-schema": "^4.1.1",
|
||||||
|
"zod": "^3.25 || ^4.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@cfworker/json-schema": {
|
"@cfworker/json-schema": {
|
||||||
"optional": true
|
"optional": true
|
||||||
|
},
|
||||||
|
"zod": {
|
||||||
|
"optional": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/jose": {
|
||||||
|
"version": "6.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
|
||||||
|
"integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@mongodb-js/saslprep": {
|
"node_modules/@mongodb-js/saslprep": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.1.tgz",
|
||||||
|
|
@ -48069,11 +48083,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod-to-json-schema": {
|
"node_modules/zod-to-json-schema": {
|
||||||
"version": "3.24.3",
|
"version": "3.25.0",
|
||||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.3.tgz",
|
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz",
|
||||||
"integrity": "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A==",
|
"integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==",
|
||||||
|
"license": "ISC",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.25 || ^4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zwitch": {
|
"node_modules/zwitch": {
|
||||||
|
|
@ -48129,7 +48144,7 @@
|
||||||
"@langchain/core": "^0.3.79",
|
"@langchain/core": "^0.3.79",
|
||||||
"@librechat/agents": "^3.0.50",
|
"@librechat/agents": "^3.0.50",
|
||||||
"@librechat/data-schemas": "*",
|
"@librechat/data-schemas": "*",
|
||||||
"@modelcontextprotocol/sdk": "^1.21.0",
|
"@modelcontextprotocol/sdk": "^1.24.3",
|
||||||
"axios": "^1.12.1",
|
"axios": "^1.12.1",
|
||||||
"connect-redis": "^8.1.0",
|
"connect-redis": "^8.1.0",
|
||||||
"diff": "^7.0.0",
|
"diff": "^7.0.0",
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@
|
||||||
"@langchain/core": "^0.3.79",
|
"@langchain/core": "^0.3.79",
|
||||||
"@librechat/agents": "^3.0.50",
|
"@librechat/agents": "^3.0.50",
|
||||||
"@librechat/data-schemas": "*",
|
"@librechat/data-schemas": "*",
|
||||||
"@modelcontextprotocol/sdk": "^1.21.0",
|
"@modelcontextprotocol/sdk": "^1.24.3",
|
||||||
"axios": "^1.12.1",
|
"axios": "^1.12.1",
|
||||||
"connect-redis": "^8.1.0",
|
"connect-redis": "^8.1.0",
|
||||||
"diff": "^7.0.0",
|
"diff": "^7.0.0",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { EModelEndpoint, extractEnvVariable, normalizeEndpointName } from 'librechat-data-provider';
|
import { EModelEndpoint, extractEnvVariable, normalizeEndpointName } from 'librechat-data-provider';
|
||||||
import type { TCustomEndpoints, TEndpoint, TConfig } from 'librechat-data-provider';
|
import type { TCustomEndpoints, TEndpoint } from 'librechat-data-provider';
|
||||||
import type { TCustomEndpointsConfig } from '~/types/endpoints';
|
import type { TCustomEndpointsConfig } from '~/types/endpoints';
|
||||||
import { isUserProvided } from '~/utils';
|
import { isUserProvided } from '~/utils';
|
||||||
|
|
||||||
|
|
@ -45,7 +45,7 @@ export function loadCustomEndpointsConfig(
|
||||||
type: EModelEndpoint.custom,
|
type: EModelEndpoint.custom,
|
||||||
userProvide: isUserProvided(resolvedApiKey),
|
userProvide: isUserProvided(resolvedApiKey),
|
||||||
userProvideURL: isUserProvided(resolvedBaseURL),
|
userProvideURL: isUserProvided(resolvedBaseURL),
|
||||||
customParams: customParams as TConfig['customParams'],
|
customParams,
|
||||||
modelDisplayLabel,
|
modelDisplayLabel,
|
||||||
iconURL,
|
iconURL,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -259,14 +259,20 @@ export class MCPConnectionFactory {
|
||||||
attempts++;
|
attempts++;
|
||||||
|
|
||||||
if (this.useOAuth && this.isOAuthError(error)) {
|
if (this.useOAuth && this.isOAuthError(error)) {
|
||||||
// Only handle OAuth if this is a user connection (has oauthStart handler)
|
// For returnOnOAuth mode, let the event handler (handleOAuthEvents) deal with OAuth
|
||||||
|
// We just need to stop retrying and let the error propagate
|
||||||
|
if (this.returnOnOAuth) {
|
||||||
|
logger.info(
|
||||||
|
`${this.logPrefix} OAuth required (return on OAuth mode), stopping retries`,
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal flow - wait for OAuth to complete
|
||||||
if (this.oauthStart && !oauthHandled) {
|
if (this.oauthStart && !oauthHandled) {
|
||||||
const errorWithFlag = error as (Error & { isOAuthError?: boolean }) | undefined;
|
oauthHandled = true;
|
||||||
if (errorWithFlag?.isOAuthError) {
|
logger.info(`${this.logPrefix} Handling OAuth`);
|
||||||
oauthHandled = true;
|
await this.handleOAuthRequired();
|
||||||
logger.info(`${this.logPrefix} Handling OAuth`);
|
|
||||||
await this.handleOAuthRequired();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Don't retry on OAuth errors - just throw
|
// Don't retry on OAuth errors - just throw
|
||||||
logger.info(`${this.logPrefix} OAuth required, stopping connection attempts`);
|
logger.info(`${this.logPrefix} OAuth required, stopping connection attempts`);
|
||||||
|
|
@ -288,15 +294,29 @@ export class MCPConnectionFactory {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for SSE error with 401 status
|
|
||||||
if ('message' in error && typeof error.message === 'string') {
|
|
||||||
return error.message.includes('401') || error.message.includes('Non-200 status code (401)');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for error code
|
// Check for error code
|
||||||
if ('code' in error) {
|
if ('code' in error) {
|
||||||
const code = (error as { code?: number }).code;
|
const code = (error as { code?: number }).code;
|
||||||
return code === 401 || code === 403;
|
if (code === 401 || code === 403) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check message for various auth error indicators
|
||||||
|
if ('message' in error && typeof error.message === 'string') {
|
||||||
|
const message = error.message.toLowerCase();
|
||||||
|
// Check for 401 status
|
||||||
|
if (message.includes('401') || message.includes('non-200 status code (401)')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Check for invalid_token (OAuth servers return this for expired/revoked tokens)
|
||||||
|
if (message.includes('invalid_token')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Check for authentication required
|
||||||
|
if (message.includes('authentication required') || message.includes('unauthorized')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -170,8 +170,6 @@ describe('formatToolContent', () => {
|
||||||
uri: 'ui://carousel',
|
uri: 'ui://carousel',
|
||||||
mimeType: 'application/json',
|
mimeType: 'application/json',
|
||||||
text: '{"items": []}',
|
text: '{"items": []}',
|
||||||
name: 'carousel',
|
|
||||||
description: 'A carousel component',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -184,8 +182,6 @@ describe('formatToolContent', () => {
|
||||||
text:
|
text:
|
||||||
'Resource Text: {"items": []}\n' +
|
'Resource Text: {"items": []}\n' +
|
||||||
'Resource URI: ui://carousel\n' +
|
'Resource URI: ui://carousel\n' +
|
||||||
'Resource: carousel\n' +
|
|
||||||
'Resource Description: A carousel component\n' +
|
|
||||||
'Resource MIME Type: application/json',
|
'Resource MIME Type: application/json',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
@ -196,8 +192,6 @@ describe('formatToolContent', () => {
|
||||||
uri: 'ui://carousel',
|
uri: 'ui://carousel',
|
||||||
mimeType: 'application/json',
|
mimeType: 'application/json',
|
||||||
text: '{"items": []}',
|
text: '{"items": []}',
|
||||||
name: 'carousel',
|
|
||||||
description: 'A carousel component',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -211,8 +205,6 @@ describe('formatToolContent', () => {
|
||||||
type: 'resource',
|
type: 'resource',
|
||||||
resource: {
|
resource: {
|
||||||
uri: 'file://document.pdf',
|
uri: 'file://document.pdf',
|
||||||
name: 'Document',
|
|
||||||
description: 'Important document',
|
|
||||||
mimeType: 'application/pdf',
|
mimeType: 'application/pdf',
|
||||||
text: 'Document content',
|
text: 'Document content',
|
||||||
},
|
},
|
||||||
|
|
@ -227,8 +219,6 @@ describe('formatToolContent', () => {
|
||||||
text:
|
text:
|
||||||
'Resource Text: Document content\n' +
|
'Resource Text: Document content\n' +
|
||||||
'Resource URI: file://document.pdf\n' +
|
'Resource URI: file://document.pdf\n' +
|
||||||
'Resource: Document\n' +
|
|
||||||
'Resource Description: Important document\n' +
|
|
||||||
'Resource MIME Type: application/pdf',
|
'Resource MIME Type: application/pdf',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
@ -242,7 +232,6 @@ describe('formatToolContent', () => {
|
||||||
type: 'resource',
|
type: 'resource',
|
||||||
resource: {
|
resource: {
|
||||||
uri: 'https://example.com/resource',
|
uri: 'https://example.com/resource',
|
||||||
name: 'Example Resource',
|
|
||||||
text: '',
|
text: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -253,7 +242,7 @@ describe('formatToolContent', () => {
|
||||||
expect(content).toEqual([
|
expect(content).toEqual([
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: 'Resource URI: https://example.com/resource\n' + 'Resource: Example Resource',
|
text: 'Resource URI: https://example.com/resource',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
expect(artifacts).toBeUndefined();
|
expect(artifacts).toBeUndefined();
|
||||||
|
|
@ -275,7 +264,6 @@ describe('formatToolContent', () => {
|
||||||
type: 'resource',
|
type: 'resource',
|
||||||
resource: {
|
resource: {
|
||||||
uri: 'file://data.csv',
|
uri: 'file://data.csv',
|
||||||
name: 'Data file',
|
|
||||||
text: '',
|
text: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -291,8 +279,7 @@ describe('formatToolContent', () => {
|
||||||
'Resource Text: {"label": "Click me"}\n' +
|
'Resource Text: {"label": "Click me"}\n' +
|
||||||
'Resource URI: ui://button\n' +
|
'Resource URI: ui://button\n' +
|
||||||
'Resource MIME Type: application/json\n\n' +
|
'Resource MIME Type: application/json\n\n' +
|
||||||
'Resource URI: file://data.csv\n' +
|
'Resource URI: file://data.csv',
|
||||||
'Resource: Data file',
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
expect(artifacts).toEqual({
|
expect(artifacts).toEqual({
|
||||||
|
|
@ -397,8 +384,6 @@ describe('formatToolContent', () => {
|
||||||
type: 'resource',
|
type: 'resource',
|
||||||
resource: {
|
resource: {
|
||||||
uri: 'https://api.example.com/data',
|
uri: 'https://api.example.com/data',
|
||||||
name: 'API Data',
|
|
||||||
description: 'External data source',
|
|
||||||
text: '',
|
text: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -417,9 +402,7 @@ describe('formatToolContent', () => {
|
||||||
'Resource Text: {"type": "bar"}\n' +
|
'Resource Text: {"type": "bar"}\n' +
|
||||||
'Resource URI: ui://chart\n' +
|
'Resource URI: ui://chart\n' +
|
||||||
'Resource MIME Type: application/json\n\n' +
|
'Resource MIME Type: application/json\n\n' +
|
||||||
'Resource URI: https://api.example.com/data\n' +
|
'Resource URI: https://api.example.com/data',
|
||||||
'Resource: API Data\n' +
|
|
||||||
'Resource Description: External data source',
|
|
||||||
},
|
},
|
||||||
{ type: 'text', text: 'Conclusion' },
|
{ type: 'text', text: 'Conclusion' },
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -759,15 +759,29 @@ export class MCPConnection extends EventEmitter {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for SSE error with 401 status
|
|
||||||
if ('message' in error && typeof error.message === 'string') {
|
|
||||||
return error.message.includes('401') || error.message.includes('Non-200 status code (401)');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for error code
|
// Check for error code
|
||||||
if ('code' in error) {
|
if ('code' in error) {
|
||||||
const code = (error as { code?: number }).code;
|
const code = (error as { code?: number }).code;
|
||||||
return code === 401 || code === 403;
|
if (code === 401 || code === 403) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check message for various auth error indicators
|
||||||
|
if ('message' in error && typeof error.message === 'string') {
|
||||||
|
const message = error.message.toLowerCase();
|
||||||
|
// Check for 401 status
|
||||||
|
if (message.includes('401') || message.includes('non-200 status code (401)')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Check for invalid_token (OAuth servers return this for expired/revoked tokens)
|
||||||
|
if (message.includes('invalid_token')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Check for authentication required
|
||||||
|
if (message.includes('authentication required') || message.includes('unauthorized')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -88,10 +88,21 @@ export class MCPOAuthHandler {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`[MCPOAuth] Discovering OAuth metadata from ${sanitizeUrlForLogging(authServerUrl)}`,
|
`[MCPOAuth] Discovering OAuth metadata from ${sanitizeUrlForLogging(authServerUrl)}`,
|
||||||
);
|
);
|
||||||
const rawMetadata = await discoverAuthorizationServerMetadata(authServerUrl, {
|
let rawMetadata = await discoverAuthorizationServerMetadata(authServerUrl, {
|
||||||
fetchFn,
|
fetchFn,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If discovery failed and we're using a path-based URL, try the base URL
|
||||||
|
if (!rawMetadata && authServerUrl.pathname !== '/') {
|
||||||
|
const baseUrl = new URL(authServerUrl.origin);
|
||||||
|
logger.debug(
|
||||||
|
`[MCPOAuth] Discovery failed with path, trying base URL: ${sanitizeUrlForLogging(baseUrl)}`,
|
||||||
|
);
|
||||||
|
rawMetadata = await discoverAuthorizationServerMetadata(baseUrl, {
|
||||||
|
fetchFn,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!rawMetadata) {
|
if (!rawMetadata) {
|
||||||
/**
|
/**
|
||||||
* No metadata discovered - create fallback metadata using default OAuth endpoint paths.
|
* No metadata discovered - create fallback metadata using default OAuth endpoint paths.
|
||||||
|
|
@ -165,6 +176,8 @@ export class MCPOAuthHandler {
|
||||||
response_types: ['code'] as string[],
|
response_types: ['code'] as string[],
|
||||||
token_endpoint_auth_method: 'client_secret_basic',
|
token_endpoint_auth_method: 'client_secret_basic',
|
||||||
scope: undefined as string | undefined,
|
scope: undefined as string | undefined,
|
||||||
|
logo_uri: undefined as string | undefined,
|
||||||
|
tos_uri: undefined as string | undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const supportedGrantTypes = metadata.grant_types_supported || ['authorization_code'];
|
const supportedGrantTypes = metadata.grant_types_supported || ['authorization_code'];
|
||||||
|
|
|
||||||
|
|
@ -56,18 +56,12 @@ function parseAsString(result: t.MCPToolCallResponse): string {
|
||||||
}
|
}
|
||||||
if (item.type === 'resource') {
|
if (item.type === 'resource') {
|
||||||
const resourceText = [];
|
const resourceText = [];
|
||||||
if (item.resource.text != null && item.resource.text) {
|
if ('text' in item.resource && item.resource.text != null && item.resource.text) {
|
||||||
resourceText.push(item.resource.text);
|
resourceText.push(item.resource.text);
|
||||||
}
|
}
|
||||||
if (item.resource.uri) {
|
if (item.resource.uri) {
|
||||||
resourceText.push(`Resource URI: ${item.resource.uri}`);
|
resourceText.push(`Resource URI: ${item.resource.uri}`);
|
||||||
}
|
}
|
||||||
if (item.resource.name) {
|
|
||||||
resourceText.push(`Resource: ${item.resource.name}`);
|
|
||||||
}
|
|
||||||
if (item.resource.description) {
|
|
||||||
resourceText.push(`Description: ${item.resource.description}`);
|
|
||||||
}
|
|
||||||
if (item.resource.mimeType != null && item.resource.mimeType) {
|
if (item.resource.mimeType != null && item.resource.mimeType) {
|
||||||
resourceText.push(`Type: ${item.resource.mimeType}`);
|
resourceText.push(`Type: ${item.resource.mimeType}`);
|
||||||
}
|
}
|
||||||
|
|
@ -143,18 +137,12 @@ export function formatToolContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourceText = [];
|
const resourceText = [];
|
||||||
if (item.resource.text != null && item.resource.text) {
|
if ('text' in item.resource && item.resource.text != null && item.resource.text) {
|
||||||
resourceText.push(`Resource Text: ${item.resource.text}`);
|
resourceText.push(`Resource Text: ${item.resource.text}`);
|
||||||
}
|
}
|
||||||
if (item.resource.uri.length) {
|
if (item.resource.uri.length) {
|
||||||
resourceText.push(`Resource URI: ${item.resource.uri}`);
|
resourceText.push(`Resource URI: ${item.resource.uri}`);
|
||||||
}
|
}
|
||||||
if (item.resource.name) {
|
|
||||||
resourceText.push(`Resource: ${item.resource.name}`);
|
|
||||||
}
|
|
||||||
if (item.resource.description) {
|
|
||||||
resourceText.push(`Resource Description: ${item.resource.description}`);
|
|
||||||
}
|
|
||||||
if (item.resource.mimeType != null && item.resource.mimeType) {
|
if (item.resource.mimeType != null && item.resource.mimeType) {
|
||||||
resourceText.push(`Resource MIME Type: ${item.resource.mimeType}`);
|
resourceText.push(`Resource MIME Type: ${item.resource.mimeType}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,16 @@ import {
|
||||||
WebSocketOptionsSchema,
|
WebSocketOptionsSchema,
|
||||||
StreamableHTTPOptionsSchema,
|
StreamableHTTPOptionsSchema,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
|
import type {
|
||||||
|
EmbeddedResource,
|
||||||
|
ListToolsResult,
|
||||||
|
ImageContent,
|
||||||
|
AudioContent,
|
||||||
|
TextContent,
|
||||||
|
Tool,
|
||||||
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
import type { SearchResultData, UIResource, TPlugin } from 'librechat-data-provider';
|
import type { SearchResultData, UIResource, TPlugin } from 'librechat-data-provider';
|
||||||
import type { TokenMethods, JsonSchemaType, IUser } from '@librechat/data-schemas';
|
import type { TokenMethods, JsonSchemaType, IUser } from '@librechat/data-schemas';
|
||||||
import type * as t from '@modelcontextprotocol/sdk/types.js';
|
|
||||||
import type { FlowStateManager } from '~/flow/manager';
|
import type { FlowStateManager } from '~/flow/manager';
|
||||||
import type { RequestBody } from '~/types/http';
|
import type { RequestBody } from '~/types/http';
|
||||||
import type * as o from '~/mcp/oauth/types';
|
import type * as o from '~/mcp/oauth/types';
|
||||||
|
|
@ -57,10 +64,10 @@ export interface MCPPrompt {
|
||||||
|
|
||||||
export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'error';
|
export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'error';
|
||||||
|
|
||||||
export type MCPTool = z.infer<typeof t.ToolSchema>;
|
export type MCPTool = Tool;
|
||||||
export type MCPToolListResponse = z.infer<typeof t.ListToolsResultSchema>;
|
export type MCPToolListResponse = ListToolsResult;
|
||||||
export type ToolContentPart = t.TextContent | t.ImageContent | t.EmbeddedResource | t.AudioContent;
|
export type ToolContentPart = TextContent | ImageContent | EmbeddedResource | AudioContent;
|
||||||
export type ImageContent = Extract<ToolContentPart, { type: 'image' }>;
|
export type { TextContent, ImageContent, EmbeddedResource, AudioContent };
|
||||||
export type MCPToolCallResponse =
|
export type MCPToolCallResponse =
|
||||||
| undefined
|
| undefined
|
||||||
| {
|
| {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import type { ClientOptions, OpenAIClientOptions } from '@librechat/agents';
|
import type { ClientOptions, OpenAIClientOptions } from '@librechat/agents';
|
||||||
import type { TEndpoint } from 'librechat-data-provider';
|
import type { TConfig } from 'librechat-data-provider';
|
||||||
import type { EndpointTokenConfig, ServerRequest } from '~/types';
|
import type { EndpointTokenConfig, ServerRequest } from '~/types';
|
||||||
|
|
||||||
export type TCustomEndpointsConfig = Partial<{ [key: string]: Omit<TEndpoint, 'order'> }>;
|
export type TCustomEndpointsConfig = Partial<{ [key: string]: Omit<TConfig, 'order'> }>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for user key values retrieved from the database
|
* Interface for user key values retrieved from the database
|
||||||
|
|
|
||||||
|
|
@ -324,7 +324,8 @@ export const endpointSchema = baseEndpointSchema.merge(
|
||||||
defaultParamsEndpoint: z.string().default('custom'),
|
defaultParamsEndpoint: z.string().default('custom'),
|
||||||
paramDefinitions: z.array(z.record(z.any())).optional(),
|
paramDefinitions: z.array(z.record(z.any())).optional(),
|
||||||
})
|
})
|
||||||
.strict(),
|
.strict()
|
||||||
|
.optional(),
|
||||||
customOrder: z.number().optional(),
|
customOrder: z.number().optional(),
|
||||||
directEndpoint: z.boolean().optional(),
|
directEndpoint: z.boolean().optional(),
|
||||||
titleMessageRole: z.string().optional(),
|
titleMessageRole: z.string().optional(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue