Skip to content
Open
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
129 changes: 129 additions & 0 deletions packages/mcp/src/functions/delete-session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import type { JulesClient } from '@google/jules-sdk';
import type { DeleteSessionOptions, DeleteSessionResult } from './types.js';

/**
* Deletes sessions by ID or filter.
*
* @param client - The Jules client instance
* @param options - Deletion options (sessionId, filter, force)
* @returns Deletion result
*/
export async function deleteSession(
client: JulesClient,
options: DeleteSessionOptions,
): Promise<DeleteSessionResult> {
const { sessionId, filter, force } = options;

if (sessionId) {
try {
await client.session(sessionId).delete();
return {
success: true,
deletedCount: 1,
sessionIds: [sessionId],
message: `Session ${sessionId} deleted successfully.`,
};
} catch (error: any) {
return {
success: false,
deletedCount: 0,
sessionIds: [],
message: `Failed to delete session ${sessionId}: ${error.message}`,
};
}
}

if (filter) {
const sessionsToDelete: string[] = [];

// Iterate through all sessions to find matches.
// By default client.sessions() excludes archived sessions, which is usually desired.
for await (const session of client.sessions()) {
let match = false;
switch (filter) {
case 'completed':
match = session.state === 'completed';
break;
case 'failed':
match = session.state === 'failed';
break;
case 'running':
match = [
'queued',
'planning',
'inProgress',
'awaitingPlanApproval',
'awaitingUserFeedback',
'paused',
].includes(session.state);
break;
case 'queued':
match = session.state === 'queued';
break;
case 'planning':
match = session.state === 'planning';
break;
case 'inProgress':
match = session.state === 'inProgress';
break;
case 'awaitingPlanApproval':
match = session.state === 'awaitingPlanApproval';
break;
}

if (match) {
sessionsToDelete.push(session.id);
}
}

if (sessionsToDelete.length === 0) {
return {
success: true,
deletedCount: 0,
sessionIds: [],
message: `No sessions found matching filter: ${filter}`,
};
}

if (!force) {
return {
success: false,
deletedCount: 0,
sessionIds: sessionsToDelete,
message: `Found ${sessionsToDelete.length} sessions matching filter "${filter}". Use 'force: true' to delete them.`,
requiresForce: true,
};
}

const deletedIds: string[] = [];
const errors: string[] = [];

// Delete in parallel with some concurrency or sequentially?
// Let's go with sequential for safety or use a simple loop.
for (const id of sessionsToDelete) {
try {
await client.session(id).delete();
deletedIds.push(id);
} catch (error: any) {
errors.push(`Failed to delete session ${id}: ${error.message}`);
}
}

return {
success: errors.length === 0,
deletedCount: deletedIds.length,
sessionIds: deletedIds,
message:
errors.length === 0
? `Successfully deleted ${deletedIds.length} sessions matching filter "${filter}".`
: `Deleted ${deletedIds.length} sessions with ${errors.length} errors. Errors: ${errors.join('; ')}`,
};
}

return {
success: false,
deletedCount: 0,
sessionIds: [],
message: 'Either sessionId or filter must be provided.',
};
}
5 changes: 5 additions & 0 deletions packages/mcp/src/functions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// Export all functions
export { getSessionState } from './session-state.js';
export { listSessions } from './list-sessions.js';
export { deleteSession } from './delete-session.js';
export { createSession } from './create-session.js';
export { interact } from './interact.js';
export { select } from './select.js';
Expand All @@ -27,6 +28,10 @@ export type {
// List Sessions
ListSessionsOptions,
ListSessionsResult,
// Delete Session
DeleteSessionFilter,
DeleteSessionOptions,
DeleteSessionResult,
// Create Session
CreateSessionOptions,
CreateSessionResult,
Expand Down
30 changes: 30 additions & 0 deletions packages/mcp/src/functions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,3 +327,33 @@ export interface WorkInProgressResult {
summary: WorkInProgressSummary;
formatted: string;
}

// ============================================================================
// Delete Session
// ============================================================================

export type DeleteSessionFilter =
| 'completed'
| 'failed'
| 'running'
| 'queued'
| 'planning'
| 'inProgress'
| 'awaitingPlanApproval';

export interface DeleteSessionOptions {
sessionId?: string;
filter?: DeleteSessionFilter;
/**
* Required when deleting multiple sessions via filter.
*/
force?: boolean;
}

export interface DeleteSessionResult {
success: boolean;
deletedCount: number;
sessionIds: string[];
message: string;
requiresForce?: boolean;
}
42 changes: 42 additions & 0 deletions packages/mcp/src/tools/delete-session.tool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { JulesClient } from '@google/jules-sdk';
import { deleteSession } from '../functions/delete-session.js';
import { defineTool, toMcpResponse } from './utils.js';

export default defineTool({
name: 'delete_session',
description: 'Deletes one or more Jules sessions.',
inputSchema: {
type: 'object',
properties: {
sessionId: {
type: 'string',
description: 'The unique ID of the session to delete.',
},
filter: {
type: 'string',
description: 'A filter to delete multiple sessions.',
enum: [
'completed',
'failed',
'running',
'queued',
'planning',
'inProgress',
'awaitingPlanApproval',
],
},
force: {
type: 'boolean',
description: 'Required when deleting multiple sessions via filter.',
},
},
},
handler: async (client: JulesClient, args: any) => {
const result = await deleteSession(client, {
sessionId: args?.sessionId,
filter: args?.filter,
force: args?.force,
});
return toMcpResponse(result);
},
});
128 changes: 128 additions & 0 deletions packages/mcp/tests/functions/delete-session.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { deleteSession } from '../../src/functions/delete-session.js';
import { createMockClient } from './helpers.js';

describe('deleteSession', () => {
let mockClient: ReturnType<typeof createMockClient>;

beforeEach(() => {
mockClient = createMockClient();
});

afterEach(() => {
vi.restoreAllMocks();
});

it('deletes a single session by ID', async () => {
const mockSession = {
delete: vi.fn().mockResolvedValue(undefined),
};
vi.spyOn(mockClient, 'session').mockReturnValue(mockSession as any);

const result = await deleteSession(mockClient, { sessionId: 'session-123' });

expect(result.success).toBe(true);
expect(result.deletedCount).toBe(1);
expect(result.sessionIds).toEqual(['session-123']);
expect(mockSession.delete).toHaveBeenCalled();
});

it('fails when single session deletion fails', async () => {
const mockSession = {
delete: vi.fn().mockRejectedValue(new Error('API Error')),
};
vi.spyOn(mockClient, 'session').mockReturnValue(mockSession as any);

const result = await deleteSession(mockClient, { sessionId: 'session-123' });

expect(result.success).toBe(false);
expect(result.deletedCount).toBe(0);
expect(result.message).toContain('Failed to delete session session-123');
});

it('deletes multiple sessions with filter and force', async () => {
const sessions = [
{ id: 's1', state: 'completed' },
{ id: 's2', state: 'failed' },
{ id: 's3', state: 'completed' },
];

vi.spyOn(mockClient, 'sessions').mockReturnValue({
[Symbol.asyncIterator]: async function* () {
for (const s of sessions) yield s;
},
} as any);

const mockDelete = vi.fn().mockResolvedValue(undefined);
vi.spyOn(mockClient, 'session').mockReturnValue({
delete: mockDelete,
} as any);

const result = await deleteSession(mockClient, {
filter: 'completed',
force: true,
});

expect(result.success).toBe(true);
expect(result.deletedCount).toBe(2);
expect(result.sessionIds).toEqual(['s1', 's3']);
expect(mockDelete).toHaveBeenCalledTimes(2);
});

it('requires force for multiple sessions', async () => {
const sessions = [{ id: 's1', state: 'completed' }];

vi.spyOn(mockClient, 'sessions').mockReturnValue({
[Symbol.asyncIterator]: async function* () {
for (const s of sessions) yield s;
},
} as any);

const result = await deleteSession(mockClient, { filter: 'completed' });

expect(result.success).toBe(false);
expect(result.requiresForce).toBe(true);
expect(result.sessionIds).toEqual(['s1']);
});

it('handles running filter correctly', async () => {
const sessions = [
{ id: 's1', state: 'inProgress' },
{ id: 's2', state: 'completed' },
{ id: 's3', state: 'queued' },
];

vi.spyOn(mockClient, 'sessions').mockReturnValue({
[Symbol.asyncIterator]: async function* () {
for (const s of sessions) yield s;
},
} as any);

const mockDelete = vi.fn().mockResolvedValue(undefined);
vi.spyOn(mockClient, 'session').mockReturnValue({
delete: mockDelete,
} as any);

const result = await deleteSession(mockClient, {
filter: 'running',
force: true,
});

expect(result.deletedCount).toBe(2);
expect(result.sessionIds).toEqual(['s1', 's3']);
});

it('returns success when no sessions match filter', async () => {
vi.spyOn(mockClient, 'sessions').mockReturnValue({
[Symbol.asyncIterator]: async function* () {
// empty
},
} as any);

const result = await deleteSession(mockClient, { filter: 'completed' });

expect(result.success).toBe(true);
expect(result.deletedCount).toBe(0);
expect(result.message).toContain('No sessions found matching filter');
});
});