Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion pkg/mcp/details.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package mcp

import (
"context"
"errors"
"fmt"
"io"

Expand All @@ -24,6 +25,18 @@ func (sm *SessionManager) GetServerDetails(ctx context.Context, userID, mcpServe
return sm.backend.getServerDetails(ctx, serverConfig.Scope)
}

// shouldAttemptLogStreaming checks if the error is one that should still have logs
// These are errors where the server might be running but not healthy
func shouldAttemptLogStreaming(err error) bool {
for unwrappedErr := err; unwrappedErr != nil; unwrappedErr = errors.Unwrap(unwrappedErr) {
switch unwrappedErr {
case ErrHealthCheckFailed, ErrHealthCheckTimeout, ErrPodConfigurationFailed:
return true
}
}
return false
}

// StreamServerLogs will stream the logs of a specific MCP server based on its configuration, if the backend supports it.
// If the server is remote, it will return an error as remote servers do not support this operation.
// If the backend does not support the operation, it will return an [ErrNotSupportedByBackend] error.
Expand All @@ -34,7 +47,10 @@ func (sm *SessionManager) StreamServerLogs(ctx context.Context, userID, mcpServe

_, err := sm.ensureDeployment(ctx, serverConfig, userID, mcpServerDisplayName, mcpServerName)
if err != nil {
return nil, err
// If error of deployment is not one that should still have logs, return the error
if !shouldAttemptLogStreaming(err) {
return nil, err
}
}

return sm.backend.streamServerLogs(ctx, serverConfig.Scope)
Expand Down
11 changes: 8 additions & 3 deletions ui/user/src/lib/components/PageLoading.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
error?: string;
errorPreContent?: Snippet;
errorPostContent?: Snippet;
errorClasses?: {
root?: string;
};
onClose?: () => void;
}

Expand All @@ -27,6 +30,7 @@
progress,
isProgressBar,
error,
errorClasses,
errorPreContent,
errorPostContent,
onClose
Expand Down Expand Up @@ -80,7 +84,10 @@
>
{#if error}
<div
class="dark:bg-surface2 dark:border-surface3 relative flex w-full flex-col items-center gap-4 rounded-lg bg-white p-4 md:w-md dark:border"
class={twMerge(
'dark:bg-surface2 dark:border-surface3 relative flex w-full flex-col items-center gap-4 rounded-lg bg-white p-4 dark:border',
errorClasses?.root
)}
use:clickOutside={() => onClose?.()}
>
<button class="icon-button absolute top-2 right-2 self-end" onclick={() => onClose?.()}
Expand All @@ -106,8 +113,6 @@
{#if errorPostContent}
{@render errorPostContent()}
{/if}

<button class="button w-full" onclick={() => onClose?.()}>Close</button>
</div>
{:else if isProgressBar}
<div
Expand Down
144 changes: 122 additions & 22 deletions ui/user/src/lib/components/mcp/MyMcpServers.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import { twMerge } from 'tailwind-merge';
import McpServerInfoAndTools from './McpServerInfoAndTools.svelte';
import PageLoading from '../PageLoading.svelte';
import { EventStreamService } from '$lib/services/admin/eventstream.svelte';

type Entry = MCPCatalogEntry & {
categories: string[]; // categories for the entry
Expand Down Expand Up @@ -101,6 +102,9 @@
let launching = $state(false);
let launchError = $state<string>();
let launchProgress = $state<number>(0);
let launchLogsEventStream = $state<EventStreamService<string>>();
let launchLogs = $state<string[]>([]);
let relaunching = $state(false);

let deletingInstance = $state<MCPServerInstance>();
let deletingServer = $state<MCPCatalogServer>();
Expand Down Expand Up @@ -250,12 +254,28 @@
deletingServer = undefined;
}

async function handleLaunchCatalogEntry(entry: Entry) {
function listLaunchLogs(mcpServerId: string) {
launchLogsEventStream = new EventStreamService<string>();
launchLogsEventStream.connect(`/api/mcp-servers/${mcpServerId}/logs`, {
onMessage: (data) => {
launchLogs = [...launchLogs, data];
}
});
}

async function handleLaunchCatalogEntry(entry: Entry, retryingServer?: MCPCatalogServer) {
if (!entry.manifest) {
console.error('No server manifest found');
return;
}

if (launchLogsEventStream) {
// reset launch logs
launchLogsEventStream.disconnect();
launchLogsEventStream = undefined;
launchLogs = [];
}

launchError = undefined;
launchProgress = 0;
launching = true;
Expand All @@ -279,14 +299,18 @@
const aliasToUse = configureForm?.name || getUniqueAlias(serverName);

let response: MCPCatalogServer | undefined = undefined;
try {
response = await ChatService.createSingleOrRemoteMcpServer({
catalogEntryID: entry.id,
manifest: url ? { remoteConfig: { url } } : {},
alias: aliasToUse
});
} catch (err) {
launchError = err instanceof Error ? err.message : 'An unknown error occurred';
if (!retryingServer) {
try {
response = await ChatService.createSingleOrRemoteMcpServer({
catalogEntryID: entry.id,
manifest: url ? { remoteConfig: { url } } : {},
alias: aliasToUse
});
} catch (err) {
launchError = err instanceof Error ? err.message : 'An unknown error occurred';
}
} else {
response = retryingServer;
}

if (response) {
Expand All @@ -301,19 +325,20 @@
configuredResponse.id
);
if (!launchResponse.success) {
// because something failed, go ahead and delete the server we created
launchError = launchResponse.message;
await ChatService.deleteSingleOrRemoteMcpServer(configuredResponse.id);
listLaunchLogs(configuredResponse.id);
} else {
launchProgress = 100;
}

selectedEntryOrServer = {
server: configuredResponse,
connectURL: configuredResponse.connectURL,
instance: undefined,
parent: entry
} as ConnectedServer;
selectedEntryOrServer = {
server: configuredResponse,
connectURL: configuredResponse.connectURL,
instance: undefined,
parent: entry
} as ConnectedServer;

if (!launchError) {
const ref = selectedEntryOrServer;
setTimeout(() => {
launching = false;
Expand All @@ -322,12 +347,13 @@
}, 1000);
}
} catch (err) {
await ChatService.deleteSingleOrRemoteMcpServer(response.id);
launchError = err instanceof Error ? err.message : 'An unknown error occurred';
} finally {
clearTimeout(timeout1);
clearTimeout(timeout2);
clearTimeout(timeout3);

relaunching = false;
}
}
}
Expand Down Expand Up @@ -401,6 +427,18 @@
if (!selectedEntryOrServer) return;
if (!configureForm) return;

if (
relaunching &&
'parent' in selectedEntryOrServer &&
'server' in selectedEntryOrServer &&
selectedEntryOrServer.parent &&
selectedEntryOrServer.server
) {
configDialog?.close();
await handleLaunchCatalogEntry(selectedEntryOrServer.parent, selectedEntryOrServer.server);
return;
}

try {
if ('server' in selectedEntryOrServer && selectedEntryOrServer.server?.id) {
if (
Expand Down Expand Up @@ -484,6 +522,23 @@
configDialog?.open();
}

async function handleCancelLaunch() {
if (launchLogsEventStream) {
launchLogsEventStream.disconnect();
}
if (
selectedEntryOrServer &&
'server' in selectedEntryOrServer &&
selectedEntryOrServer.server
) {
await ChatService.deleteSingleOrRemoteMcpServer(selectedEntryOrServer.server.id);
selectedEntryOrServer = selectedEntryOrServer.parent;
}

launching = false;
launchError = undefined;
}

const duration = PAGE_TRANSITION_DURATION;
</script>

Expand Down Expand Up @@ -654,17 +709,62 @@
text="Configuring and initializing server..."
progress={launchProgress}
error={launchError}
onClose={() => {
launching = false;
errorClasses={{
root: 'md:w-[95vw]'
}}
onClose={handleCancelLaunch}
>
{#snippet errorPreContent()}
<h4 class="text-xl font-semibold">MCP Server Launch Failed</h4>
{/snippet}
{#snippet errorPostContent()}
<p class="text-md self-start">An issue occurred while launching the MCP server.</p>
{@const hasConfigurableParent =
selectedEntryOrServer &&
'parent' in selectedEntryOrServer &&
selectedEntryOrServer.parent &&
hasEditableConfiguration(selectedEntryOrServer.parent)}
{#if launchLogs.length > 0}
<div
class="default-scrollbar-thin bg-surface1 max-h-[50vh] w-full overflow-y-auto rounded-lg p-4 shadow-inner"
>
{#each launchLogs as log, i (i)}
<div class="font-mono text-sm">
<span class="text-gray-600 dark:text-gray-400">{log}</span>
</div>
{/each}
</div>
{:else}
<p class="text-md self-start">An issue occurred while launching the MCP server.</p>
{/if}

<p class="text-md self-start">If the problem persists, please contact an administrator.</p>
<div class="flex w-full flex-col items-center gap-2 md:flex-row">
{#if hasConfigurableParent}
<button
class="button-primary w-full md:w-1/2 md:flex-1"
onclick={() => {
launching = false;
launchError = undefined;
relaunching = true;
if (
selectedEntryOrServer &&
'parent' in selectedEntryOrServer &&
selectedEntryOrServer.parent
) {
if (hasEditableConfiguration(selectedEntryOrServer.parent)) {
configDialog?.open();
} else {
handleLaunch();
}
}
}}
>
Update Configuration and Try Again
</button>
{/if}
<button class="button w-full md:w-1/2 md:flex-1" onclick={handleCancelLaunch}>
Cancel and Delete Server
</button>
</div>
{/snippet}
</PageLoading>

Expand Down
7 changes: 7 additions & 0 deletions ui/user/src/lib/services/chat/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1625,6 +1625,13 @@ export async function validateSingleOrRemoteMcpServerLaunched(mcpServerId: strin
}
}

export async function listSingleOrRemoteMcpServerLogs(mcpServerId: string): Promise<string[]> {
const response = (await doGet(`/mcp-servers/${mcpServerId}/logs`, {
dontLogErrors: true
})) as ItemsResponse<string>;
return response.items ?? [];
}

export async function listWorkspaces(opts?: { fetch?: Fetcher }): Promise<Workspace[]> {
const response = (await doGet('/workspaces', opts)) as ItemsResponse<Workspace>;
return response.items ?? [];
Expand Down