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
36 changes: 36 additions & 0 deletions src/__tests__/mcp-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,42 @@ describe('MCP Tool Handlers', () => {
expect(result.content[0].text).toContain('Session not found');
});

it('get_status handler includes pendingQuestion when session is ask_question', async () => {
let callCount = 0;
const ts = Date.now();
(fetch as any).mockImplementation(() => {
callCount++;
if (callCount === 1) {
return {
ok: true,
json: () => Promise.resolve({
id: UUID,
status: 'ask_question',
pendingQuestion: {
toolUseId: 'toolu_abc123',
content: 'Which strategy? 1) Clean up 2) Auto-restart 3) Both',
options: ['Clean up', 'Auto-restart', 'Both'],
since: ts,
},
}),
};
}
return { ok: true, json: () => Promise.resolve({ alive: true, status: 'ask_question' }) };
});

const handler = getToolHandler('get_status');
const result = await handler({ sessionId: UUID });
expect(result.isError).toBeFalsy();
const data = parseResult(result);
expect(data.status).toBe('ask_question');
expect(data.pendingQuestion).toEqual({
toolUseId: 'toolu_abc123',
content: 'Which strategy? 1) Clean up 2) Auto-restart 3) Both',
options: ['Clean up', 'Auto-restart', 'Both'],
since: ts,
});
});

// ── get_transcript handler ──

it('get_transcript handler returns transcript', async () => {
Expand Down
34 changes: 31 additions & 3 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,12 +577,12 @@ app.post('/sessions', async (req, reply) => {
app.get<{ Params: { id: string } }>('/v1/sessions/:id', async (req, reply) => {
const session = sessions.getSession(req.params.id);
if (!session) return reply.status(404).send({ error: 'Session not found' });
return addActionHints(session);
return addActionHints(session, sessions);
});
app.get<{ Params: { id: string } }>('/sessions/:id', async (req, reply) => {
const session = sessions.getSession(req.params.id);
if (!session) return reply.status(404).send({ error: 'Session not found' });
return addActionHints(session);
return addActionHints(session, sessions);
});

// #128: Bulk health check — returns health for all sessions in one request
Expand Down Expand Up @@ -1276,7 +1276,10 @@ async function reapZombieSessions(): Promise<void> {
// ── Helpers ──────────────────────────────────────────────────────────

/** Issue #20: Add actionHints to session response for interactive states. */
function addActionHints(session: import('./session.js').SessionInfo): Record<string, unknown> {
function addActionHints(
session: import('./session.js').SessionInfo,
sessions?: SessionManager,
): Record<string, unknown> {
// #357: Convert Set to array for JSON serialization
const result: Record<string, unknown> = {
...session,
Expand All @@ -1288,9 +1291,34 @@ function addActionHints(session: import('./session.js').SessionInfo): Record<str
reject: { method: 'POST', url: `/v1/sessions/${session.id}/reject`, description: 'Reject the pending permission' },
};
}
// #599: Expose pending question data for MCP/REST callers
if (session.status === 'ask_question' && sessions) {
const info = sessions.getPendingQuestionInfo(session.id);
if (info) {
result.pendingQuestion = {
toolUseId: info.toolUseId,
content: info.question,
options: extractQuestionOptions(info.question),
since: info.timestamp,
};
}
}
return result;
}

/** #599: Extract selectable options from AskUserQuestion text. */
function extractQuestionOptions(text: string): string[] | null {
// Numbered options: "1. Foo\n2. Bar"
const numberedRegex = /^\s*(\d+)\.\s+(.+)$/gm;
const options: string[] = [];
let m;
while ((m = numberedRegex.exec(text)) !== null) {
options.push(m[2].trim());
}
if (options.length >= 2) return options.slice(0, 4);
return null;
}

function makePayload(
event: SessionEvent,
sessionId: string,
Expand Down
7 changes: 4 additions & 3 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ interface PendingQuestion {
timer: NodeJS.Timeout;
toolUseId: string;
question: string;
timestamp: number;
}

export class SessionManager {
Expand Down Expand Up @@ -955,7 +956,7 @@ export class SessionManager {
resolve(null);
}, timeoutMs);

this.pendingQuestions.set(sessionId, { resolve, timer, toolUseId, question });
this.pendingQuestions.set(sessionId, { resolve, timer, toolUseId, question, timestamp: Date.now() });
});
}

Expand All @@ -976,9 +977,9 @@ export class SessionManager {
}

/** Issue #336: Get info about a pending question. */
getPendingQuestionInfo(sessionId: string): { toolUseId: string; question: string } | null {
getPendingQuestionInfo(sessionId: string): { toolUseId: string; question: string; timestamp: number } | null {
const pending = this.pendingQuestions.get(sessionId);
return pending ? { toolUseId: pending.toolUseId, question: pending.question } : null;
return pending ? { toolUseId: pending.toolUseId, question: pending.question, timestamp: pending.timestamp } : null;
}

/** Issue #336: Clean up any pending question for a session. */
Expand Down