diff --git a/deno.lock b/deno.lock index 6e08692..906039f 100644 --- a/deno.lock +++ b/deno.lock @@ -1,7 +1,7 @@ { "version": "5", "specifiers": { - "jsr:@beyondbetter/bb-mcp-server@~0.1.17": "0.1.17", + "jsr:@beyondbetter/bb-mcp-server@~0.1.18": "0.1.18", "jsr:@fresh/build-id@1": "1.0.1", "jsr:@fresh/core@^2.1.2": "2.1.3", "jsr:@std/cli@^1.0.16": "1.0.23", @@ -32,8 +32,8 @@ "npm:zod@^3.22.4": "3.25.76" }, "jsr": { - "@beyondbetter/bb-mcp-server@0.1.17": { - "integrity": "63c7dfd5591c24e3a999fa5d687707a4a5a52ee6d0b67c1aee4bc8362d219feb", + "@beyondbetter/bb-mcp-server@0.1.18": { + "integrity": "206a92689fe89b6fde738399e62c7cf8ee2877ebb6ef24850ab31bbab8e3afcf", "dependencies": [ "jsr:@std/datetime", "jsr:@std/dotenv", @@ -54,21 +54,6 @@ "jsr:@std/encoding" ] }, - "@fresh/core@2.1.2": { - "integrity": "ffa92d2306e39136cab53f51b6bb25391c9eea3d5fa989382003e4579101ca78", - "dependencies": [ - "jsr:@fresh/build-id", - "jsr:@std/fmt@^1.0.8", - "jsr:@std/fs", - "jsr:@std/html", - "jsr:@std/http", - "jsr:@std/path@1", - "npm:@opentelemetry/api", - "npm:@preact/signals", - "npm:preact", - "npm:preact-render-to-string" - ] - }, "@fresh/core@2.1.3": { "integrity": "5656afa01607af5ca4693abdec3da9cfb41618f52daac11c00846de0994fd7e7", "dependencies": [ @@ -722,7 +707,7 @@ }, "workspace": { "dependencies": [ - "jsr:@beyondbetter/bb-mcp-server@~0.1.17", + "jsr:@beyondbetter/bb-mcp-server@~0.1.18", "jsr:@fresh/core@^2.1.2", "jsr:@fresh/plugin-vite@^1.0.5", "jsr:@std/assert@^1.0.12", diff --git a/fresh-ui/components/ClientSelector.tsx b/fresh-ui/components/ClientSelector.tsx index 506f530..c21697d 100644 --- a/fresh-ui/components/ClientSelector.tsx +++ b/fresh-ui/components/ClientSelector.tsx @@ -11,15 +11,30 @@ import { selectedClientId } from '../hooks/useConsoleState.ts'; import type { ClientInfo, SessionId } from '@shared/types/index.ts'; import { isClientList } from '@shared/types/index.ts'; +/** + * Format a timestamp to a readable date/time string + */ +function formatTimestamp(timestamp: number): string { + const date = new Date(timestamp); + return date.toLocaleString('en-US', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }); +} + export default function ClientSelector() { const detailsRef = useRef(null); const dropdownContentRef = useRef(null); const summaryRef = useRef(null); - // Extract clients from messages + // Extract clients from messages and sort by most recently seen const clientListMessage = wsMessages.value.find(isClientList); const clients = clientListMessage ? (clientListMessage.payload as { clients: ClientInfo[] }).clients + .sort((a, b) => b.lastSeen - a.lastSeen) // Sort by most recent first : []; // Auto-select first client if none selected and clients available @@ -206,6 +221,18 @@ export default function ClientSelector() { {client.sessionId.slice(0, 8)}... + {/* Connection Times */} +
+
+ Connected:{' '} + {formatTimestamp(client.connectedAt)} +
+
+ Last Seen:{' '} + {formatTimestamp(client.lastSeen)} +
+
+ {/* Last Meta Indicator */} {metadata?.lastMeta && (
@@ -246,9 +273,23 @@ export default function ClientSelector() { )} {/* Selection Info */} - {selectedClientId.value && ( -
- Selected: {selectedClientId.value} + {selectedClient && ( +
+
Selected Client
+
+
+ Session ID:{' '} + {selectedClient.sessionId} +
+
+ Connected:{' '} + {formatTimestamp(selectedClient.connectedAt)} +
+
+ Last Seen:{' '} + {formatTimestamp(selectedClient.lastSeen)} +
+
)}
diff --git a/fresh-ui/components/ElicitationForm.tsx b/fresh-ui/components/ElicitationForm.tsx index 14f47f0..e47e6c3 100644 --- a/fresh-ui/components/ElicitationForm.tsx +++ b/fresh-ui/components/ElicitationForm.tsx @@ -112,7 +112,7 @@ export default function ElicitationForm() { type: 'object', properties: {}, // Empty properties for approval mode }, - sessionId: selectedClientId.value || undefined, // Target specific client + options: { sessionId: selectedClientId.value || undefined }, // Target specific client }; // Add schema if in form mode and schema provided @@ -127,6 +127,7 @@ export default function ElicitationForm() { } } + //console.log('[ElicitationForm] Validating payload:', payload); // Validate payload with Zod const validation = validateElicitationPayload(payload); if (!validation.success) { @@ -134,7 +135,7 @@ export default function ElicitationForm() { return; } - console.log('[ElicitationForm] Sending request:', payload); + console.log('[ElicitationForm] Sending request:', validation.data); sendCommand({ type: 'request_elicitation', payload: validation.data, diff --git a/fresh-ui/components/NotificationForm.tsx b/fresh-ui/components/NotificationForm.tsx index e28eab4..c225cd2 100644 --- a/fresh-ui/components/NotificationForm.tsx +++ b/fresh-ui/components/NotificationForm.tsx @@ -20,7 +20,7 @@ export default function NotificationForm() { level: notificationLevel.value, logger: notificationLogger.value || undefined, data: notificationMessage.value, - sessionId: selectedClientId.value || undefined, // Target specific client + options: { sessionId: selectedClientId.value || undefined }, // Target specific client }; sendCommand({ diff --git a/fresh-ui/components/SamplingForm.tsx b/fresh-ui/components/SamplingForm.tsx index d0de4bf..8100077 100644 --- a/fresh-ui/components/SamplingForm.tsx +++ b/fresh-ui/components/SamplingForm.tsx @@ -40,7 +40,7 @@ export default function SamplingForm() { : undefined, temperature: temperature.value, maxTokens: maxTokens.value, - sessionId: selectedClientId.value || undefined, // Target specific client + options: { sessionId: selectedClientId.value || undefined }, // Target specific client }; console.log('[SamplingForm] Sending request:', payload); diff --git a/import_map.json b/import_map.json index e5e8a2a..612da4e 100644 --- a/import_map.json +++ b/import_map.json @@ -3,8 +3,8 @@ "@shared/types/": "./shared/types/", "@shared/version.ts": "./shared/version.ts", - "@beyondbetter/bb-mcp-server": "jsr:@beyondbetter/bb-mcp-server@^0.1.17", - "@beyondbetter/bb-mcp-server/": "jsr:@beyondbetter/bb-mcp-server@^0.1.17/", + "@beyondbetter/bb-mcp-server": "jsr:@beyondbetter/bb-mcp-server@^0.1.18", + "@beyondbetter/bb-mcp-server/": "jsr:@beyondbetter/bb-mcp-server@^0.1.18/", "mcp": "npm:@modelcontextprotocol/sdk@1.18.2", "mcp/client/": "npm:@modelcontextprotocol/sdk@1.18.2/client/", diff --git a/mcp-server/deno.lock b/mcp-server/deno.lock index 9e06db7..3bbdee4 100644 --- a/mcp-server/deno.lock +++ b/mcp-server/deno.lock @@ -1,7 +1,6 @@ { "version": "5", "specifiers": { - "jsr:@beyondbetter/bb-mcp-server@~0.1.17": "0.1.17", "jsr:@std/datetime@~0.225.4": "0.225.5", "jsr:@std/dotenv@~0.225.3": "0.225.5", "jsr:@std/fmt@^1.0.6": "1.0.8", @@ -17,22 +16,6 @@ "npm:zod@^3.22.4": "3.25.76" }, "jsr": { - "@beyondbetter/bb-mcp-server@0.1.17": { - "integrity": "63c7dfd5591c24e3a999fa5d687707a4a5a52ee6d0b67c1aee4bc8362d219feb", - "dependencies": [ - "jsr:@std/datetime", - "jsr:@std/dotenv", - "jsr:@std/fmt", - "jsr:@std/path", - "npm:@modelcontextprotocol/sdk", - "npm:gray-matter", - "npm:highlight.js", - "npm:marked", - "npm:marked-highlight", - "npm:zod", - "npm:zod-to-json-schema" - ] - }, "@std/datetime@0.225.5": { "integrity": "9f650f6caec546b80172e95a4edb8478d5fe060c4c937f7ede242ffceab6efc9" }, @@ -628,7 +611,6 @@ }, "workspace": { "dependencies": [ - "jsr:@beyondbetter/bb-mcp-server@~0.1.17", "jsr:@fresh/core@^2.1.2", "jsr:@fresh/plugin-vite@^1.0.5", "jsr:@std/assert@^1.0.12", diff --git a/mcp-server/src/console/ConsoleManager.ts b/mcp-server/src/console/ConsoleManager.ts index b230f18..8216f23 100644 --- a/mcp-server/src/console/ConsoleManager.ts +++ b/mcp-server/src/console/ConsoleManager.ts @@ -235,7 +235,7 @@ export class ConsoleManager { logger: payload.logger, data: payload.data, }, - payload.sessionId, + payload.options, ); this.broadcastMessage({ @@ -273,7 +273,7 @@ export class ConsoleManager { ): Promise { try { this.logger.info('ConsoleManager: Requesting sampling from client', { - sessionId: payload.sessionId, + options: payload.options, }); // Convert to library's CreateMessageRequest format @@ -289,7 +289,7 @@ export class ConsoleManager { maxTokens: payload.maxTokens, temperature: payload.temperature, stopSequences: payload.stopSequences, - }, payload.sessionId); // Pass sessionId for client targeting + }, payload.options); // Pass options for client targeting this.broadcastMessage({ type: 'sampling_response', @@ -321,24 +321,24 @@ export class ConsoleManager { ): Promise { try { this.logger.info('ConsoleManager: Requesting elicitation from client', { - sessionId: payload.sessionId, + options: payload.options, }); this.logger.debug('ConsoleManager: Elicitation request payload:', { message: payload.message, requestedSchema: payload.requestedSchema, - sessionId: payload.sessionId, + options: payload.options, }); const response = await beyondMcpServer.elicitInput({ message: payload.message, requestedSchema: payload.requestedSchema, - }, payload.sessionId); + }, payload.options); this.logger.debug('ConsoleManager: Elicitation response received:', { response: JSON.stringify(response, null, 2), responseType: typeof response, responseKeys: response ? Object.keys(response) : [], - }); // Pass sessionId for client targeting + }); // Pass options for client targeting this.broadcastMessage({ type: 'elicitation_response', @@ -355,7 +355,7 @@ export class ConsoleManager { payloadSent: { message: payload.message, requestedSchema: payload.requestedSchema, - sessionId: payload.sessionId, + sessionId: payload.options.sessionId, }, }, ); @@ -476,7 +476,7 @@ export class ConsoleManager { ): Promise { try { const messages = await this.messageTracker.getMessages( - payload.sessionId || 'default', + payload.options.sessionId || 'default', payload.limit || 100, ); diff --git a/shared/types/console.types.ts b/shared/types/console.types.ts index cb68fb5..67ceb6c 100644 --- a/shared/types/console.types.ts +++ b/shared/types/console.types.ts @@ -120,7 +120,7 @@ export interface NotificationPayload { /** * Optional session ID for targeting specific client */ - sessionId?: SessionId; + options: { sessionId?: SessionId; meta?: Record }; } /** @@ -151,7 +151,7 @@ export interface SamplingPayload { /** * Optional session ID for targeting specific client */ - sessionId?: SessionId; + options: { sessionId?: SessionId; meta?: Record }; } /** @@ -204,7 +204,7 @@ export interface ElicitationPayload { /** * Optional session ID for targeting specific client */ - sessionId?: SessionId; + options: { sessionId?: SessionId; meta?: Record }; } /** diff --git a/shared/types/validation.ts b/shared/types/validation.ts index 7c5f8cc..3cbcab2 100644 --- a/shared/types/validation.ts +++ b/shared/types/validation.ts @@ -36,7 +36,10 @@ export const notificationPayloadSchema = z.object({ ]), logger: z.string().optional(), data: z.unknown(), - sessionId: z.string().optional(), + options: z.object({ + sessionId: z.string().optional(), + meta: z.record(z.unknown()).optional(), + }), }); /** @@ -125,6 +128,10 @@ const elicitationSchemaSchema = z.object({ export const elicitationPayloadSchema = z.object({ message: z.string().min(1, 'Message is required'), requestedSchema: elicitationSchemaSchema, + options: z.object({ + sessionId: z.string().optional(), + meta: z.record(z.unknown()).optional(), + }), }); /** diff --git a/tests/deno.lock b/tests/deno.lock index 1024a5e..e59770b 100644 --- a/tests/deno.lock +++ b/tests/deno.lock @@ -1,7 +1,7 @@ { "version": "5", "specifiers": { - "jsr:@beyondbetter/bb-mcp-server@~0.1.17": "0.1.17", + "jsr:@beyondbetter/bb-mcp-server@~0.1.18": "0.1.18", "jsr:@std/assert@^1.0.12": "1.0.15", "jsr:@std/assert@^1.0.15": "1.0.15", "jsr:@std/datetime@~0.225.4": "0.225.5", @@ -22,8 +22,8 @@ "npm:zod@^3.22.4": "3.25.76" }, "jsr": { - "@beyondbetter/bb-mcp-server@0.1.17": { - "integrity": "63c7dfd5591c24e3a999fa5d687707a4a5a52ee6d0b67c1aee4bc8362d219feb", + "@beyondbetter/bb-mcp-server@0.1.18": { + "integrity": "206a92689fe89b6fde738399e62c7cf8ee2877ebb6ef24850ab31bbab8e3afcf", "dependencies": [ "jsr:@std/datetime", "jsr:@std/dotenv", @@ -659,7 +659,7 @@ }, "workspace": { "dependencies": [ - "jsr:@beyondbetter/bb-mcp-server@~0.1.17", + "jsr:@beyondbetter/bb-mcp-server@~0.1.18", "jsr:@fresh/core@^2.1.2", "jsr:@fresh/plugin-vite@^1.0.5", "jsr:@std/assert@^1.0.12", diff --git a/tests/fixtures/console-messages.ts b/tests/fixtures/console-messages.ts index b6f6de7..e842d43 100644 --- a/tests/fixtures/console-messages.ts +++ b/tests/fixtures/console-messages.ts @@ -62,6 +62,7 @@ export const notificationPayload: NotificationPayload = { level: 'info', logger: 'test', data: 'Test notification message', + options: {}, }; /** @@ -100,6 +101,7 @@ export const samplingPayload: SamplingPayload = { ], maxTokens: 100, temperature: 0.7, + options: {}, }; /** @@ -153,6 +155,7 @@ export const elicitationPayload: ElicitationPayload = { }, required: ['confirmed'], }, + options: {}, }; /** diff --git a/tests/integration/message-flow.test.ts b/tests/integration/message-flow.test.ts index 5b8a1cc..59f48e5 100644 --- a/tests/integration/message-flow.test.ts +++ b/tests/integration/message-flow.test.ts @@ -99,13 +99,13 @@ describe('Message Flow Integration', () => { logger: 'test', data: 'Test notification', }, - sessionId, + { sessionId }, ); const notifications = mockServer.getNotifications(); assertEquals(notifications.length, 1); assertEquals(notifications[0].level, 'info'); - assertEquals(notifications[0].sessionId, sessionId); + assertEquals(notifications[0].options.sessionId, sessionId); }); it('should handle multiple notifications', async () => { @@ -116,7 +116,7 @@ describe('Message Flow Integration', () => { for (const level of levels) { await mockServer.sendNotification( { level, logger: 'test', data: `${level} message` }, - sessionId, + { sessionId }, ); } @@ -140,7 +140,7 @@ describe('Message Flow Integration', () => { ], maxTokens: 100, }, - sessionId, + { sessionId }, ); assertExists(response); @@ -149,7 +149,7 @@ describe('Message Flow Integration', () => { const requests = mockServer.getSamplingRequests(); assertEquals(requests.length, 1); - assertEquals(requests[0].sessionId, sessionId); + assertEquals(requests[0].options.sessionId, sessionId); }); it('should handle sampling with model preferences', async () => { @@ -159,7 +159,7 @@ describe('Message Flow Integration', () => { { role: 'user', content: { type: 'text', text: 'test' } }, ], maxTokens: 50, - }); + }, {}); assertEquals(response.model, 'test-model'); }); @@ -175,7 +175,7 @@ describe('Message Flow Integration', () => { ], maxTokens: 100, }, - sessionId, + { sessionId }, ); } @@ -198,7 +198,7 @@ describe('Message Flow Integration', () => { }, }, }, - sessionId, + { sessionId }, ); assertExists(response); @@ -207,13 +207,13 @@ describe('Message Flow Integration', () => { const requests = mockServer.getElicitationRequests(); assertEquals(requests.length, 1); - assertEquals(requests[0].sessionId, sessionId); + assertEquals(requests[0].options.sessionId, sessionId); }); it('should handle elicitation without schema', async () => { const response = await mockServer.elicitInput({ message: 'Simple confirmation', - }); + }, {}); assertEquals(response.action, 'accept'); }); @@ -261,8 +261,8 @@ describe('Message Flow Integration', () => { jsonrpc: '2.0' as const, method: 'test2', }), - mockServer.sendNotification({ level: 'info', data: 'test' }, session1), - mockServer.sendNotification({ level: 'info', data: 'test' }, session2), + mockServer.sendNotification({ level: 'info', data: 'test' }, { sessionId: session1 }), + mockServer.sendNotification({ level: 'info', data: 'test' }, { sessionId: session2 }), ]); const messages1 = await messageTracker.getMessages(session1); diff --git a/tests/integration/websocket-communication.test.ts b/tests/integration/websocket-communication.test.ts index 5c57ef5..874afb6 100644 --- a/tests/integration/websocket-communication.test.ts +++ b/tests/integration/websocket-communication.test.ts @@ -109,7 +109,7 @@ describe('WebSocket Communication Integration', () => { }, }; - await mockServer.sendNotification(command.payload as any); + await mockServer.sendNotification(command.payload as any, {}); const notifications = mockServer.getNotifications(); assertEquals(notifications.length, 1); @@ -133,7 +133,7 @@ describe('WebSocket Communication Integration', () => { const response = await mockServer.createMessage({ messages: (command.payload as any).messages, maxTokens: (command.payload as any).maxTokens, - }); + }, {}); assertExists(response); assertEquals(response.content[0].type, 'text'); @@ -156,7 +156,7 @@ describe('WebSocket Communication Integration', () => { }, }; - const response = await mockServer.elicitInput(command.payload as any); + const response = await mockServer.elicitInput(command.payload as any, {}); assertExists(response); assertEquals(response.action, 'accept'); diff --git a/tests/unit/mcp-server/console/ConsoleManager.test.ts b/tests/unit/mcp-server/console/ConsoleManager.test.ts index ba9381b..584deca 100644 --- a/tests/unit/mcp-server/console/ConsoleManager.test.ts +++ b/tests/unit/mcp-server/console/ConsoleManager.test.ts @@ -79,7 +79,7 @@ describe('ConsoleManager', () => { level: payload.level, logger: payload.logger, data: payload.data, - }); + }, {}); const notifications = mockServer.getNotifications(); assertEquals(notifications.length, 1); @@ -100,12 +100,12 @@ describe('ConsoleManager', () => { logger: payload.logger, data: payload.data, }, - sessionId, + { sessionId }, ); const notifications = mockServer.getNotifications(); assertEquals(notifications.length, 1); - assertEquals(notifications[0].sessionId, sessionId); + assertEquals(notifications[0].options.sessionId, sessionId); }); }); @@ -116,7 +116,7 @@ describe('ConsoleManager', () => { const response = await mockServer.createMessage({ messages: payload.messages, maxTokens: payload.maxTokens, - }); + }, {}); assertExists(response); assertExists(response.content); @@ -132,12 +132,12 @@ describe('ConsoleManager', () => { messages: payload.messages, maxTokens: payload.maxTokens, }, - sessionId, + { sessionId }, ); const requests = mockServer.getSamplingRequests(); assertEquals(requests.length, 1); - assertEquals(requests[0].sessionId, sessionId); + assertEquals(requests[0].options.sessionId, sessionId); }); it('should include model preferences in request', async () => { @@ -154,7 +154,7 @@ describe('ConsoleManager', () => { model: payload.modelPreferences.hints[0].name, messages: payload.messages, maxTokens: payload.maxTokens, - }); + }, {}); const requests = mockServer.getSamplingRequests(); assertEquals(requests.length, 1); @@ -168,7 +168,7 @@ describe('ConsoleManager', () => { const response = await mockServer.elicitInput({ message: payload.message, requestedSchema: payload.requestedSchema, - }); + }, {}); assertExists(response); assertEquals(response.action, 'accept'); @@ -183,12 +183,12 @@ describe('ConsoleManager', () => { message: payload.message, requestedSchema: payload.requestedSchema, }, - sessionId, + { sessionId }, ); const requests = mockServer.getElicitationRequests(); assertEquals(requests.length, 1); - assertEquals(requests[0].sessionId, sessionId); + assertEquals(requests[0].options.sessionId, sessionId); }); it('should include requested schema', async () => { @@ -205,7 +205,7 @@ describe('ConsoleManager', () => { }, }; - await mockServer.elicitInput(payload); + await mockServer.elicitInput(payload, {}); const requests = mockServer.getElicitationRequests(); assertEquals(requests.length, 1); diff --git a/tests/utils/mocks.ts b/tests/utils/mocks.ts index b2446e7..c74d176 100644 --- a/tests/utils/mocks.ts +++ b/tests/utils/mocks.ts @@ -13,23 +13,32 @@ import type { ClientId, SessionId } from '@shared/types/index.ts'; */ export class MockBeyondMcpServer implements Partial { private notifications: Array< - { level: string; logger?: string; data: unknown; sessionId?: string } + { + level: string; + logger?: string; + data: unknown; + options: { sessionId?: string; meta?: Record }; + } + > = []; + private samplingRequests: Array< + { messages: unknown[]; options: { sessionId?: string; meta?: Record } } + > = []; + private elicitationRequests: Array< + { message: string; options: { sessionId?: string; meta?: Record } } > = []; - private samplingRequests: Array<{ messages: unknown[]; sessionId?: string }> = []; - private elicitationRequests: Array<{ message: string; sessionId?: string }> = []; // deno-lint-ignore require-await async sendNotification( params: { level: string; logger?: string; data: unknown }, - sessionId?: string, + options: { sessionId?: string; meta?: Record }, ): Promise { - this.notifications.push({ ...params, sessionId }); + this.notifications.push({ ...params, options }); } // deno-lint-ignore require-await async createMessage( request: { model?: string; messages: unknown[]; maxTokens?: number }, - sessionId?: string, + options: { sessionId?: string; meta?: Record }, ): Promise< { content: { type: 'text'; text: string }[]; @@ -37,7 +46,7 @@ export class MockBeyondMcpServer implements Partial { stopReason: string; } > { - this.samplingRequests.push({ messages: request.messages, sessionId }); + this.samplingRequests.push({ messages: request.messages, options }); return { content: [{ type: 'text', text: 'Mock response' }], model: request.model || 'mock-model', @@ -48,9 +57,9 @@ export class MockBeyondMcpServer implements Partial { // deno-lint-ignore require-await async elicitInput( request: { message: string; requestedSchema?: unknown }, - sessionId?: string, + options: { sessionId?: string; meta?: Record }, ): Promise<{ action: 'accept' | 'reject'; content?: unknown }> { - this.elicitationRequests.push({ message: request.message, sessionId }); + this.elicitationRequests.push({ message: request.message, options }); return { action: 'accept' as const, content: { confirmed: true },