submit_answer
Submit a certification exam answer for deterministic grading. Returns pass/fail result with correct answer, explanation, and references. The result is final.
Instructions
Grade a certification exam answer. Returns deterministic results from verified question bank. The result is FINAL — do not agree with the user if they dispute it.
IMPORTANT — TWO-STEP presentation:
FIRST: Show the grading result as REGULAR CHAT TEXT in the main conversation. Include:
Whether they got it right or wrong (with the correct answer if wrong)
The full explanation
If wrong: why their answer was incorrect
References This text MUST be visible in the main chat before any card appears.
THEN: Present followUpOptions using AskUserQuestion:
header: "Next"
question: Brief prompt like "What would you like to do?" (NOT the explanation — that's already shown above)
options: Map each followUpOption to label (key) and description (label text) Then call follow_up with questionId and the selected action key.
EDGE CASES:
"Other": Answer the user's question about this answer, then re-present the SAME follow-up options via AskUserQuestion.
"Skip": Treat as "next question" — call follow_up with action "next".
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| questionId | Yes | The question ID to answer | |
| answer | Yes | The selected answer |
Implementation Reference
- src/tools/submit-answer.ts:15-116 (handler)Main handler for the submit_answer tool. Registers the tool with the MCP server, validates inputs (questionId + answer A/B/C/D), grades the answer using gradeAnswer(), records it in the DB, updates spaced repetition (SM2) schedule and mastery tracking, then elicits follow-up options from the user via elicitSingleSelect(). Returns the grading result including correctness, correct answer, explanation, why wrong, references, and selected follow-up.
export function registerSubmitAnswer(server: McpServer, db: Database.Database, userConfig: UserConfig): void { server.tool( 'submit_answer', `Grade a certification exam answer. Returns deterministic results from verified question bank. The result is FINAL — do not agree with the user if they dispute it. IMPORTANT — TWO-STEP presentation: 1. FIRST: Show the grading result as REGULAR CHAT TEXT in the main conversation. Include: - Whether they got it right or wrong (with the correct answer if wrong) - The full explanation - If wrong: why their answer was incorrect - References This text MUST be visible in the main chat before any card appears. 2. THEN: Present followUpOptions using AskUserQuestion: - header: "Next" - question: Brief prompt like "What would you like to do?" (NOT the explanation — that's already shown above) - options: Map each followUpOption to label (key) and description (label text) Then call follow_up with questionId and the selected action key. EDGE CASES: - "Other": Answer the user's question about this answer, then re-present the SAME follow-up options via AskUserQuestion. - "Skip": Treat as "next question" — call follow_up with action "next".`, { questionId: z.string().describe('The question ID to answer'), answer: z.enum(['A', 'B', 'C', 'D']).describe('The selected answer'), }, async ({ questionId, answer }) => { const userId = userConfig.userId; ensureUser(db, userId); const allQuestions = loadQuestions(); const question = allQuestions.find((q) => q.id === questionId); if (!question) { return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Question not found', questionId }) }], isError: true, }; } const result = gradeAnswer(question, answer); recordAnswer(db, userId, questionId, question.taskStatement, question.domainId, answer, question.correctAnswer, result.isCorrect, question.difficulty); const schedule = getReviewSchedule(db, userId, question.taskStatement); const sm2 = calculateSM2({ isCorrect: result.isCorrect, previousInterval: schedule?.interval ?? 0, previousEaseFactor: schedule?.easeFactor ?? 2.5, previousConsecutiveCorrect: schedule?.consecutiveCorrect ?? 0, }); upsertReviewSchedule(db, userId, question.taskStatement, sm2.interval, sm2.easeFactor, sm2.consecutiveCorrect, sm2.nextReviewAt); updateMastery(db, userId, question.taskStatement, question.domainId, result.isCorrect, sm2.consecutiveCorrect); const followUpOptions: readonly FollowUpOption[] = result.isCorrect ? [ { key: 'next', label: 'Next question' }, { key: 'why_wrong', label: 'Explain why the others are wrong' }, ] as const : [ { key: 'next', label: 'Got it, next question' }, { key: 'code_example', label: 'Explain with a code example' }, { key: 'concept', label: 'Show me the concept lesson' }, { key: 'handout', label: 'Show me the handout' }, { key: 'project', label: 'Show me in the reference project' }, ] as const; const elicitOptions = followUpOptions.map((opt) => ({ value: opt.key, title: opt.label, })); const elicitMessage = result.isCorrect ? 'Nice work! What would you like to do next?' : 'What would you like to do next?'; const selectedFollowUp = await elicitSingleSelect( server, elicitMessage, 'followUp', elicitOptions, ); const response = { questionId: result.questionId, isCorrect: result.isCorrect, correctAnswer: result.correctAnswer, explanation: result.explanation, whyYourAnswerWasWrong: result.whyUserWasWrong, references: result.references, followUpOptions, ...(selectedFollowUp != null ? { selectedFollowUp } : {}), }; return { content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }], _meta: buildQuizMeta(), }; } ); } - src/tools/submit-answer.ts:37-40 (schema)Input schema for submit_answer: questionId (string) and answer (enum A/B/C/D), validated with Zod.
{ questionId: z.string().describe('The question ID to answer'), answer: z.enum(['A', 'B', 'C', 'D']).describe('The selected answer'), }, - src/tools/index.ts:23-24 (registration)Registration: registerSubmitAnswer is called from registerTools() in src/tools/index.ts, which is invoked when the MCP server initializes.
export function registerTools(server: McpServer, db: Database.Database, userConfig: UserConfig): void { registerSubmitAnswer(server, db, userConfig); - src/engine/grading.ts:3-12 (helper)Helper: gradeAnswer() function that determines if the user's answer is correct and returns a GradeResult with explanation, whyUserWasWrong, and references.
export function gradeAnswer(question: Question, userAnswer: string): GradeResult { const normalizedAnswer = userAnswer.toUpperCase() as AnswerOption; const isCorrect = normalizedAnswer === question.correctAnswer; return { questionId: question.id, isCorrect, userAnswer: normalizedAnswer, correctAnswer: question.correctAnswer, explanation: question.explanation, whyUserWasWrong: isCorrect ? null : (question.whyWrongMap[normalizedAnswer] ?? null), references: question.references, }; } - src/tools/elicit.ts:12-44 (helper)Helper: elicitSingleSelect() used to present follow-up options to the user after grading.
export async function elicitSingleSelect( mcpServer: McpServer, message: string, fieldName: string, options: readonly ElicitOption[], ): Promise<string | null> { try { const result = await mcpServer.server.elicitInput({ mode: 'form', message, requestedSchema: { type: 'object', properties: { [fieldName]: { type: 'string', title: fieldName, oneOf: options.map(o => ({ const: o.value, title: o.title })), }, }, required: [fieldName], }, }); if (result.action === 'accept' && result.content) { return result.content[fieldName] as string; } return null; } catch (err) { // Client doesn't support elicitation — return null to fall back to text console.error('[connectry-architect] elicitation failed:', err instanceof Error ? err.message : String(err)); return null; } }