Skip to content

Commit 36a4922

Browse files
committed
feat(desktop): add copy-all to server log viewer
Also reconcile May 23 closeout notes in meta-tools and account-clones planning docs. Signed-off-by: crimsonsunset <jsangio1@gmail.com>
1 parent 308963b commit 36a4922

3 files changed

Lines changed: 64 additions & 15 deletions

File tree

apps/desktop/src/components/ServerLogViewer.tsx

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect, useState, useRef } from 'react';
2-
import { X, Download, Trash2, RefreshCw } from 'lucide-react';
2+
import { X, Download, Trash2, RefreshCw, Copy } from 'lucide-react';
33
import { useToast, ToastContainer, useConfirm } from '@mcpmux/ui';
44
import { getServerLogs, clearServerLogs, getServerLogFile, type ServerLogEntry } from '@/lib/api/logs';
55

@@ -32,6 +32,30 @@ const SOURCE_COLORS: Record<string, string> = {
3232
server: 'text-cyan-400',
3333
};
3434

35+
/**
36+
* Formats an ISO timestamp for log display and export.
37+
*/
38+
function formatTimestamp(ts: string): string {
39+
const date = new Date(ts);
40+
const hours = date.getHours().toString().padStart(2, '0');
41+
const minutes = date.getMinutes().toString().padStart(2, '0');
42+
const seconds = date.getSeconds().toString().padStart(2, '0');
43+
const ms = date.getMilliseconds().toString().padStart(3, '0');
44+
return `${hours}:${minutes}:${seconds}.${ms}`;
45+
}
46+
47+
/**
48+
* Formats a log entry as a single plain-text line for display export.
49+
*/
50+
function formatLogLine(log: ServerLogEntry): string {
51+
const level = log.level.toUpperCase().padEnd(5);
52+
const base = `${formatTimestamp(log.timestamp)} ${level} ${log.source} ${log.message}`;
53+
if (!log.metadata) {
54+
return base;
55+
}
56+
return `${base} ${JSON.stringify(log.metadata)}`;
57+
}
58+
3559
export function ServerLogViewer({ serverId, serverName, onClose }: ServerLogViewerProps) {
3660
const [logs, setLogs] = useState<ServerLogEntry[]>([]);
3761
const [loading, setLoading] = useState(true);
@@ -122,22 +146,32 @@ export function ServerLogViewer({ serverId, serverName, onClose }: ServerLogView
122146
}
123147
};
124148

125-
const formatTimestamp = (ts: string) => {
126-
const date = new Date(ts);
127-
const hours = date.getHours().toString().padStart(2, '0');
128-
const minutes = date.getMinutes().toString().padStart(2, '0');
129-
const seconds = date.getSeconds().toString().padStart(2, '0');
130-
const ms = date.getMilliseconds().toString().padStart(3, '0');
131-
return `${hours}:${minutes}:${seconds}.${ms}`;
132-
};
133-
134149
const filteredLogs = logs.filter(log => {
135150
if (levelFilter === 'all') return true;
136151
const logLevelIndex = LOG_LEVELS.indexOf(log.level as LogLevel);
137152
const filterLevelIndex = LOG_LEVELS.indexOf(levelFilter);
138153
return logLevelIndex >= filterLevelIndex;
139154
});
140155

156+
/** Copies all currently visible (filtered) log lines to the clipboard. */
157+
const handleCopyAll = async () => {
158+
if (filteredLogs.length === 0) {
159+
showError('Nothing to copy', 'No logs match the current filter');
160+
return;
161+
}
162+
163+
try {
164+
const text = filteredLogs.map((log) => formatLogLine(log)).join('\n');
165+
await navigator.clipboard.writeText(text);
166+
success(
167+
'Logs copied',
168+
`${filteredLogs.length} log${filteredLogs.length !== 1 ? 's' : ''} copied to clipboard`
169+
);
170+
} catch (e) {
171+
showError('Failed to copy logs', e instanceof Error ? e.message : String(e));
172+
}
173+
};
174+
141175
return (
142176
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
143177
<ToastContainer toasts={toasts} onClose={dismiss} />
@@ -195,6 +229,16 @@ export function ServerLogViewer({ serverId, serverName, onClose }: ServerLogView
195229
>
196230
<Download className="h-4 w-4" />
197231
</button>
232+
233+
{/* Copy All */}
234+
<button
235+
onClick={handleCopyAll}
236+
disabled={filteredLogs.length === 0}
237+
className="p-1.5 rounded-lg hover:bg-[rgb(var(--surface-hover))] transition-colors disabled:opacity-40 disabled:pointer-events-none"
238+
title="Copy all visible logs to clipboard"
239+
>
240+
<Copy className="h-4 w-4" />
241+
</button>
198242

199243
{/* Clear Logs */}
200244
<button

docs/planning/dynamic-mcp-toggle-meta-tools.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Dynamic MCP Toggling via Meta Tools
22

3-
**Last Updated:** May 19, 2026
4-
**Status:** Feature complete — pending validation + PR
3+
**Last Updated:** May 23, 2026
4+
**Status:** Feature complete on fork — session grants + structuredContent fixes verified; pending upstream PR merge
55
**Branch:** `feat/dynamic-mcp-toggle-meta-tools`
66
**Base branch:** `feat/workspace-root-routing` ([upstream PR #151](https://github.com/mcpmux/mcp-mux/pull/151))
77
**Issue:** TBD — file after planning review
@@ -298,6 +298,7 @@ Optional (slow / env-dependent): `pnpm test:e2e`, `pnpm test:e2e:web`.
298298

299299
- [mcpmux/mcp-mux PR #151](https://github.com/mcpmux/mcp-mux/pull/151) — workspace-root-driven FeatureSet routing + the `mcpmux_*` meta-tool namespace this PR builds on. Must merge (or be consumed via fork) first.
300300
- [`docs/planning/issue-52-secret-text-input-syntax.md`](./issue-52-secret-text-input-syntax.md) — sibling planning doc; same conventions used here. Independent feature, no functional overlap.
301+
- [`docs/planning/server-account-clones.md`](./server-account-clones.md) — multi-account installs via UI clone; branched after meta-tools + gateway fixes.
301302
- [`jsg-tech-check` homelab plan](../../../jsg-tech-check/docs/setup/home-lab-overview.md#mcp-strategy--current-state) — the consuming use case. The "Personal vs Work" Spaces + bundled `set-times-app` / `sync2hire-platform` model leans on bindings; this doc adds the "no, actually just enable this one MCP for the next 10 minutes" escape valve.
302303
- [MCP spec — Tools `list_changed`](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#list-changed-notification) — the protocol mechanism that makes the post-write tool-list refresh observable mid-conversation. Already wired by PR #151.
303304

@@ -307,8 +308,10 @@ Optional (slow / env-dependent): `pnpm test:e2e`, `pnpm test:e2e:web`.
307308

308309
This doc is the source of truth for what gets built. When implementation completes, update the **Status** field at the top and reconcile any deviations (extra files, dropped phases, scope changes) per [`update-planning-md`](~/.cursor/commands/update-planning-md.md).
309310

310-
**May 19, 2026 closeout:**
311+
**May 23, 2026 closeout:**
311312
- Phases 1–5 implemented on `feat/dynamic-mcp-toggle-meta-tools`.
313+
- Post-ship fixes on same branch: session-grant routing in `call_tool` (`5269a18`), `structuredContent` forwarding for proxied tool results (`d519a79`) — required for Google Workspace MCP through mux.
312314
- Tauri commands landed in `session_overrides.rs` (not `workspace_binding.rs` as originally planned).
313315
- CHANGELOG handled by release-please; README updated in-repo.
314316
- Pre-PR validation gate documented above; PR blocked until validate + tests + build pass.
317+
- Follow-on: [server-account-clones.md](./server-account-clones.md) branched from this work for multi-account migration.

docs/planning/server-account-clones.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Server Account Clones (UI-Assisted Multi-Account)
22

33
**Last Updated:** May 23, 2026
4-
**Status:** In progress — Phases 1–4 complete, Phase 5 optional
5-
**Branch:** `feat/server-account-clones`
4+
**Status:** Complete — Phases 1–4 shipped; Phase 5 (first-class instances) optional/deferred
5+
**Branch:** `feat/server-account-clones` (merged or ready to merge)
66
**Base branch:** `main`
77
**Issue:** TBD — file after planning review
88
**Depends on:** None (orthogonal to session meta-tools; benefits from but does not require PR #154)
@@ -266,3 +266,5 @@ Multi-account need?
266266
This doc is the source of truth for server account clones. When implementation starts, update **Status** and **Branch** at the top. Phase 5 remains optional — do not block Phases 1–4 on it.
267267

268268
**Decision record (May 23, 2026):** Option 2 (UI-assisted clone) selected over status quo, first-class instances (deferred Phase 5), per-client credential override (rejected), and wrapper meta-servers (rejected). Brainstorm source: Cursor session on multi-account MCP patterns.
269+
270+
**May 23, 2026 closeout:** Phases 1–4 implemented and verified in dev. Consumed by [mcpmux-server-migration.md](../../../jsg-tech-check/docs/setup/mcpmux-server-migration.md) Phase B2 (clone sprint). Phase 5 remains optional until clone ID sprawl or FeatureSet grouping pain justifies schema migration.

0 commit comments

Comments
 (0)