import {
TOOLS,
type ToolResult,
type GeminiToolArgs,
type PingToolArgs,
GeminiToolSchema,
PingToolSchema,
HelpToolSchema,
ListSessionsToolSchema,
} from '../types.js';
import {
InMemorySessionStorage,
type SessionStorage,
type ConversationTurn,
} from '../session/storage.js';
import { ToolExecutionError, ValidationError } from '../errors.js';
import { executeCommand } from '../utils/command.js';
import { ZodError } from 'zod';
export class GeminiToolHandler {
constructor(private sessionStorage: SessionStorage) {}
async execute(args: unknown): Promise<ToolResult> {
try {
const { prompt, sessionId, resetSession, model }: GeminiToolArgs =
GeminiToolSchema.parse(args);
let activeSessionId = sessionId;
let enhancedPrompt = prompt;
// Only work with sessions if explicitly requested
let geminiConversationId: string | undefined;
if (sessionId) {
if (resetSession) {
this.sessionStorage.resetSession(sessionId);
}
geminiConversationId =
this.sessionStorage.getGeminiConversationId(sessionId);
if (!geminiConversationId) {
// Fallback to manual context building if no gemini conversation ID
const session = this.sessionStorage.getSession(sessionId);
if (
session &&
Array.isArray(session.turns) &&
session.turns.length > 0
) {
enhancedPrompt = this.buildEnhancedPrompt(session.turns, prompt);
}
}
}
// Build command arguments for Gemini CLI
const cmdArgs: string[] = ['-o', 'stream-json'];
// Enable auto-approval for tool execution (write_file, etc.)
// Required for non-interactive mode to use tools like write_file
cmdArgs.push('--yolo');
// Add include directories for file write access (configurable via env)
const includeDirs = process.env.GEMINI_INCLUDE_DIRECTORIES;
if (includeDirs) {
cmdArgs.push('--include-directories', includeDirs);
}
// Add model parameter (only if specified, otherwise let Gemini CLI auto-select)
const selectedModel = model || 'gemini-2.5-flash'; // Default to gemini-2.5-flash
if (selectedModel) {
cmdArgs.push('-m', selectedModel);
}
// Add conversation ID if resuming
if (geminiConversationId) {
cmdArgs.push('-c', geminiConversationId);
}
// Add prompt (must be last)
cmdArgs.push('-p', enhancedPrompt);
const result = await executeCommand('gemini', cmdArgs);
// Parse Gemini's stream-json output
const { response, conversationId } = this.parseGeminiOutput(result.stdout);
// Store conversation ID for future resume
if (activeSessionId && conversationId) {
this.sessionStorage.setGeminiConversationId(
activeSessionId,
conversationId
);
}
// Save turn only if using a session
if (activeSessionId) {
const turn: ConversationTurn = {
prompt,
response,
timestamp: new Date(),
};
this.sessionStorage.addTurn(activeSessionId, turn);
}
return {
content: [
{
type: 'text',
text: response,
},
],
_meta: {
...(activeSessionId && { sessionId: activeSessionId }),
model: selectedModel,
},
};
} catch (error) {
if (error instanceof ZodError) {
throw new ValidationError(TOOLS.GEMINI, error.message);
}
throw new ToolExecutionError(
TOOLS.GEMINI,
'Failed to execute gemini command',
error
);
}
}
private parseGeminiOutput(stdout: string): {
response: string;
conversationId?: string;
} {
let response = '';
let conversationId: string | undefined;
const messages: string[] = [];
// Parse each line as JSON
const lines = stdout.split('\n').filter((line) => line.trim());
for (const line of lines) {
try {
const event = JSON.parse(line);
// Extract session_id from init event (Gemini CLI uses "type" and "session_id")
if (event.type === 'init' && event.session_id) {
conversationId = event.session_id;
}
// Collect message content from assistant messages
if (
event.type === 'message' &&
event.role === 'assistant' &&
event.content
) {
messages.push(event.content);
}
// Check for result event (Gemini CLI doesn't have text field in result)
// We'll rely on message collection instead
} catch (e) {
// Ignore parse errors for individual lines
console.error('Failed to parse Gemini output line:', line);
}
}
// Concatenate all assistant messages
if (messages.length > 0) {
response = messages.join('');
}
return { response: response || 'No output from Gemini', conversationId };
}
private buildEnhancedPrompt(
turns: ConversationTurn[],
newPrompt: string
): string {
if (turns.length === 0) return newPrompt;
// Get relevant context from recent turns
const recentTurns = turns.slice(-2);
const contextualInfo = recentTurns
.map((turn) => {
// Extract key information without conversational format
if (
turn.response.includes('function') ||
turn.response.includes('def ')
) {
return `Previous code context: ${turn.response.slice(0, 200)}...`;
}
return `Context: ${turn.prompt} -> ${turn.response.slice(0, 100)}...`;
})
.join('\n');
// Build enhanced prompt that provides context without conversation format
return `${contextualInfo}\n\nTask: ${newPrompt}`;
}
}
export class PingToolHandler {
async execute(args: unknown): Promise<ToolResult> {
try {
const { message = 'pong' }: PingToolArgs = PingToolSchema.parse(args);
return {
content: [
{
type: 'text',
text: message,
},
],
};
} catch (error) {
if (error instanceof ZodError) {
throw new ValidationError(TOOLS.PING, error.message);
}
throw new ToolExecutionError(
TOOLS.PING,
'Failed to execute ping command',
error
);
}
}
}
export class HelpToolHandler {
async execute(args: unknown): Promise<ToolResult> {
try {
HelpToolSchema.parse(args);
// ⭐ ENHANCED HELP - Provides detailed documentation for compressed schemas
const documentation = `
# Gemini Daily MCP - Detailed Documentation
## gemini Tool
Execute Gemini CLI in non-interactive mode for AI assistance.
**Parameters:**
- prompt (required, string): The coding task, question, or analysis request
- sessionId (optional, string): Session ID for conversational context
- Saves up to 74% tokens in multi-turn conversations
- Obtained from previous gemini call's _meta.sessionId
- resetSession (optional, boolean): Reset session history before processing
- Default: false
- model (optional, string): Specify which Gemini model to use
- Default: gemini-2.5-flash
- Options: gemini-2.5-flash, gemini-2.5-flash-lite, gemini-2.5-pro, gemini-3-pro-preview
- See MODELS.md for details
**Example:**
{
"prompt": "Write a Python function to calculate fibonacci",
"sessionId": "session-abc123",
"model": "gemini-2.5-flash"
}
**Returns:**
- text: Gemini's response
- _meta.sessionId: Session ID for subsequent calls
- _meta.model: Model used
## ping Tool
Test MCP server connection.
**Parameters:**
- message (optional, string): Message to echo back
**Returns:** Echo of the message
## listSessions Tool
List all active conversation sessions with metadata.
**Returns:** Array of sessions with IDs, timestamps, and message counts
## Gemini CLI Help
${(await executeCommand('gemini', ['--help'])).stdout || 'CLI help unavailable'}
`;
return {
content: [
{
type: 'text',
text: documentation,
},
],
};
} catch (error) {
if (error instanceof ZodError) {
throw new ValidationError(TOOLS.HELP, error.message);
}
throw new ToolExecutionError(
TOOLS.HELP,
'Failed to execute help command',
error
);
}
}
}
export class ListSessionsToolHandler {
constructor(private sessionStorage: SessionStorage) {}
async execute(args: unknown): Promise<ToolResult> {
try {
ListSessionsToolSchema.parse(args);
const sessions = this.sessionStorage.listSessions();
const sessionInfo = sessions.map((session) => ({
id: session.id,
createdAt: session.createdAt.toISOString(),
lastAccessedAt: session.lastAccessedAt.toISOString(),
turnCount: session.turns.length,
}));
return {
content: [
{
type: 'text',
text:
sessionInfo.length > 0
? JSON.stringify(sessionInfo, null, 2)
: 'No active sessions',
},
],
};
} catch (error) {
if (error instanceof ZodError) {
throw new ValidationError(TOOLS.LIST_SESSIONS, error.message);
}
throw new ToolExecutionError(
TOOLS.LIST_SESSIONS,
'Failed to list sessions',
error
);
}
}
}
// Tool handler registry
const sessionStorage = new InMemorySessionStorage();
export const toolHandlers = {
[TOOLS.GEMINI]: new GeminiToolHandler(sessionStorage),
[TOOLS.PING]: new PingToolHandler(),
[TOOLS.HELP]: new HelpToolHandler(),
[TOOLS.LIST_SESSIONS]: new ListSessionsToolHandler(sessionStorage),
} as const;