@longtable/mcp 0.1.31 → 0.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +14 -4
  2. package/dist/server.js +479 -4
  3. package/package.json +7 -6
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # @longtable/mcp
2
2
 
3
- MCP transport for LongTable workspace state and Researcher Checkpoints.
3
+ MCP transport for LongTable workspace state, `$longtable-interview`, and
4
+ Researcher Checkpoints.
4
5
 
5
6
  This package does not own LongTable state. It exposes structured tools over the
6
7
  existing `.longtable/` source of truth.
@@ -14,7 +15,7 @@ longtable-state
14
15
  Run:
15
16
 
16
17
  ```bash
17
- npx -y @longtable/mcp@0.1.31
18
+ npx -y @longtable/mcp@0.1.32
18
19
  ```
19
20
 
20
21
  Self-test:
@@ -32,5 +33,14 @@ longtable mcp install --provider codex --checkpoint-ui strong --write
32
33
  If MCP elicitation is unavailable or not approved, the server returns the same
33
34
  pending `QuestionRecord` as a numbered fallback.
34
35
 
35
- Provider guidance should use `elicit_question` first when the MCP tool is
36
- available. `longtable question --print` is only the CLI fallback transport.
36
+ Provider guidance should use interview tools for `$longtable-interview`:
37
+
38
+ - `create_workspace`
39
+ - `begin_interview`
40
+ - `append_interview_turn`
41
+ - `summarize_interview`
42
+ - `confirm_first_research_shape`
43
+
44
+ For later Researcher Checkpoints, provider guidance should use
45
+ `elicit_question` first when the MCP tool is available. `longtable
46
+ question --print` is only the CLI fallback transport.
package/dist/server.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFile, writeFile } from "node:fs/promises";
2
2
  import { existsSync } from "node:fs";
3
- import { resolve } from "node:path";
3
+ import { basename, resolve } from "node:path";
4
4
  import { cwd, exit } from "node:process";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -9,13 +9,19 @@ import { z } from "zod";
9
9
  import { classifyCheckpointTrigger } from "@longtable/checkpoints";
10
10
  import { renderQuestionRecordInput } from "@longtable/provider-claude";
11
11
  import { renderQuestionRecordPrompt } from "@longtable/provider-codex";
12
- import { answerWorkspaceQuestion, createWorkspaceQuestion, inspectProjectWorkspace, loadProjectContextFromDirectory, loadWorkspaceState, syncCurrentWorkspaceView } from "@longtable/cli";
12
+ import { loadSetupOutput } from "@longtable/setup";
13
+ import { answerWorkspaceQuestion, createOrUpdateProjectWorkspace, createWorkspaceQuestion, inspectProjectWorkspace, loadProjectContextFromDirectory, loadWorkspaceState, syncCurrentWorkspaceView } from "@longtable/cli";
13
14
  const SERVER_NAME = "longtable-state";
14
- const SERVER_VERSION = "0.1.31";
15
+ const SERVER_VERSION = "0.1.32";
15
16
  const TOOL_NAMES = [
16
17
  "read_project",
17
18
  "read_session",
18
19
  "inspect_workspace",
20
+ "create_workspace",
21
+ "begin_interview",
22
+ "append_interview_turn",
23
+ "summarize_interview",
24
+ "confirm_first_research_shape",
19
25
  "pending_questions",
20
26
  "evaluate_checkpoint",
21
27
  "create_question",
@@ -33,6 +39,17 @@ const questionOptionSchema = z.object({
33
39
  description: z.string().optional(),
34
40
  recommended: z.boolean().optional()
35
41
  });
42
+ const firstResearchShapeSchema = z.object({
43
+ handle: z.string().min(1),
44
+ currentGoal: z.string().min(1),
45
+ currentBlocker: z.string().optional(),
46
+ researchObject: z.string().optional(),
47
+ gapRisk: z.string().optional(),
48
+ protectedDecision: z.string().optional(),
49
+ openQuestions: z.array(z.string().min(1)).default([]),
50
+ nextAction: z.string().min(1),
51
+ confidence: z.enum(["low", "medium", "high"]).default("medium")
52
+ });
36
53
  function textResult(structuredContent) {
37
54
  return {
38
55
  content: [
@@ -65,6 +82,196 @@ async function requireContext(startPath) {
65
82
  }
66
83
  return context;
67
84
  }
85
+ function createId(prefix) {
86
+ return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
87
+ }
88
+ function asInterviewState(state) {
89
+ return state;
90
+ }
91
+ function activeInterviewHook(state, hookId) {
92
+ const hooks = state.hooks ?? [];
93
+ if (hookId) {
94
+ return hooks.find((hook) => hook.id === hookId);
95
+ }
96
+ return [...hooks].reverse().find((hook) => hook.kind === "longtable_interview" &&
97
+ (hook.status === "pending" || hook.status === "active" || hook.status === "ready_to_confirm"));
98
+ }
99
+ function upsertInterviewHook(state, hook) {
100
+ const hooks = state.hooks ?? [];
101
+ const nextHooks = hooks.some((candidate) => candidate.id === hook.id)
102
+ ? hooks.map((candidate) => candidate.id === hook.id ? hook : candidate)
103
+ : [...hooks, hook];
104
+ return {
105
+ ...state,
106
+ hooks: nextHooks
107
+ };
108
+ }
109
+ function interviewDepth(turns) {
110
+ const usableTurns = turns.filter((turn) => turn.quality !== "thin").length;
111
+ if (usableTurns >= 3)
112
+ return "ready_to_summarize";
113
+ if (usableTurns >= 1)
114
+ return "forming_first_handle";
115
+ return "gathering_context";
116
+ }
117
+ function normalizeInterviewQuality(answer, quality) {
118
+ if (quality)
119
+ return quality;
120
+ const trimmed = answer.trim();
121
+ const wordCount = trimmed.split(/\s+/).filter(Boolean).length;
122
+ if (trimmed.length < 12 || wordCount < 3)
123
+ return "thin";
124
+ if (trimmed.length > 80 || wordCount >= 12)
125
+ return "rich";
126
+ return "usable";
127
+ }
128
+ function defaultFollowUpQuestion(answer) {
129
+ return answer.trim().length < 12
130
+ ? "Say one more sentence about where this problem appears or why it matters before LongTable tries to classify it."
131
+ : "What concrete scene, case, material, text, dataset, or decision would make that problem easier to inspect first?";
132
+ }
133
+ async function beginInterviewHook(context, options) {
134
+ const state = asInterviewState(await loadWorkspaceState(context));
135
+ const existing = activeInterviewHook(state);
136
+ if (existing) {
137
+ return { hook: existing, state };
138
+ }
139
+ const timestamp = new Date().toISOString();
140
+ const hook = {
141
+ id: createId("hook_interview"),
142
+ kind: "longtable_interview",
143
+ status: "active",
144
+ createdAt: timestamp,
145
+ updatedAt: timestamp,
146
+ targetOutcome: "first_research_handle",
147
+ depth: "gathering_context",
148
+ provider: options.provider,
149
+ turns: [],
150
+ qualityNotes: [],
151
+ rationale: [
152
+ "Official LongTable research start surface is provider-native `$longtable-interview`, not the CLI start questionnaire."
153
+ ]
154
+ };
155
+ const updated = upsertInterviewHook(state, hook);
156
+ updated.workingState = {
157
+ ...updated.workingState,
158
+ activeInterviewHookId: hook.id,
159
+ interviewSurface: "$longtable-interview",
160
+ ...(options.openingQuestion ? { interviewOpeningQuestion: options.openingQuestion } : {}),
161
+ ...(options.seedAnswer ? { interviewSeedAnswer: options.seedAnswer } : {})
162
+ };
163
+ await writeFile(context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
164
+ await syncCurrentWorkspaceView(context);
165
+ return { hook, state: updated };
166
+ }
167
+ async function appendInterviewTurn(context, options) {
168
+ const state = asInterviewState(await loadWorkspaceState(context));
169
+ const existing = activeInterviewHook(state, options.hookId);
170
+ if (!existing) {
171
+ throw new Error("No active LongTable interview hook was found. Run begin_interview first.");
172
+ }
173
+ const quality = normalizeInterviewQuality(options.answer, options.quality);
174
+ const needsFollowUp = options.needsFollowUp ?? quality === "thin";
175
+ const followUpQuestion = needsFollowUp
176
+ ? options.followUpQuestion ?? defaultFollowUpQuestion(options.answer)
177
+ : options.followUpQuestion;
178
+ const timestamp = new Date().toISOString();
179
+ const turn = {
180
+ id: createId("interview_turn"),
181
+ index: existing.turns.length + 1,
182
+ createdAt: timestamp,
183
+ question: options.question.trim(),
184
+ answer: options.answer.trim(),
185
+ ...(options.reflection?.trim() ? { reflection: options.reflection.trim() } : {}),
186
+ quality,
187
+ needsFollowUp,
188
+ ...(followUpQuestion?.trim() ? { followUpQuestion: followUpQuestion.trim() } : {}),
189
+ ...(options.rationale && options.rationale.length > 0 ? { rationale: options.rationale } : {})
190
+ };
191
+ const turns = [...existing.turns, turn];
192
+ const depth = interviewDepth(turns);
193
+ const hook = {
194
+ ...existing,
195
+ status: depth === "ready_to_summarize" ? "ready_to_confirm" : "active",
196
+ updatedAt: timestamp,
197
+ depth,
198
+ turns,
199
+ qualityNotes: [
200
+ ...existing.qualityNotes,
201
+ ...(needsFollowUp ? [`Turn ${turn.index} needs follow-up: ${followUpQuestion}`] : [])
202
+ ]
203
+ };
204
+ const updated = upsertInterviewHook(state, hook);
205
+ await writeFile(context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
206
+ await syncCurrentWorkspaceView(context);
207
+ return { hook, turn, state: updated };
208
+ }
209
+ async function summarizeInterviewHook(context, options) {
210
+ const state = asInterviewState(await loadWorkspaceState(context));
211
+ const existing = activeInterviewHook(state, options.hookId);
212
+ if (!existing) {
213
+ throw new Error("No active LongTable interview hook was found. Run begin_interview first.");
214
+ }
215
+ const timestamp = new Date().toISOString();
216
+ const shape = {
217
+ ...options.shape,
218
+ handle: options.shape.handle.trim(),
219
+ currentGoal: options.shape.currentGoal.trim(),
220
+ openQuestions: options.shape.openQuestions.map((question) => question.trim()).filter(Boolean),
221
+ nextAction: options.shape.nextAction.trim(),
222
+ sourceHookId: existing.id
223
+ };
224
+ const hook = {
225
+ ...existing,
226
+ status: "ready_to_confirm",
227
+ updatedAt: timestamp,
228
+ depth: "ready_to_summarize",
229
+ firstResearchShape: shape
230
+ };
231
+ const session = {
232
+ ...context.session,
233
+ lastUpdatedAt: timestamp,
234
+ currentGoal: shape.currentGoal,
235
+ ...(shape.currentBlocker ? { currentBlocker: shape.currentBlocker } : {}),
236
+ ...(shape.researchObject ? { researchObject: shape.researchObject } : {}),
237
+ ...(shape.gapRisk ? { gapRisk: shape.gapRisk } : {}),
238
+ ...(shape.protectedDecision ? { protectedDecision: shape.protectedDecision } : {}),
239
+ nextAction: shape.nextAction,
240
+ openQuestions: shape.openQuestions,
241
+ firstResearchShape: shape,
242
+ resumeHint: `I want to continue from the First Research Shape: ${shape.handle}.`
243
+ };
244
+ context.session = session;
245
+ const updated = upsertInterviewHook(state, hook);
246
+ updated.firstResearchShape = shape;
247
+ updated.workingState = {
248
+ ...updated.workingState,
249
+ currentGoal: shape.currentGoal,
250
+ ...(shape.currentBlocker ? { currentBlocker: shape.currentBlocker } : {}),
251
+ ...(shape.researchObject ? { researchObject: shape.researchObject } : {}),
252
+ ...(shape.gapRisk ? { gapRisk: shape.gapRisk } : {}),
253
+ ...(shape.protectedDecision ? { protectedDecision: shape.protectedDecision } : {}),
254
+ openQuestions: shape.openQuestions,
255
+ nextAction: shape.nextAction,
256
+ firstResearchShape: shape
257
+ };
258
+ if (shape.currentBlocker && !updated.openTensions.includes(shape.currentBlocker)) {
259
+ updated.openTensions.push(shape.currentBlocker);
260
+ }
261
+ updated.narrativeTraces.push({
262
+ id: createId("narrative_trace"),
263
+ timestamp,
264
+ source: "$longtable-interview",
265
+ traceType: "judgment",
266
+ summary: `First Research Shape: ${shape.handle}.`,
267
+ visibility: "explicit",
268
+ importance: shape.confidence
269
+ });
270
+ await writeFile(context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
271
+ await writeFile(context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
272
+ await syncCurrentWorkspaceView(context);
273
+ return { hook, shape, state: updated, session };
274
+ }
68
275
  function findQuestion(records, questionId) {
69
276
  if (questionId) {
70
277
  return records.find((record) => record.id === questionId) ?? null;
@@ -77,7 +284,7 @@ function renderQuestionFallback(record, provider = "codex") {
77
284
  : renderQuestionRecordPrompt(record);
78
285
  }
79
286
  async function markQuestionTransport(context, questionId, status, message) {
80
- const state = await loadWorkspaceState(context);
287
+ const state = asInterviewState(await loadWorkspaceState(context));
81
288
  let updatedQuestion = null;
82
289
  state.questionLog = (state.questionLog ?? []).map((record) => {
83
290
  if (record.id !== questionId) {
@@ -148,6 +355,66 @@ function acceptedAnswer(result) {
148
355
  answer
149
356
  };
150
357
  }
358
+ function buildFirstResearchShapeQuestion(shape) {
359
+ return {
360
+ prompt: [
361
+ "Confirm the LongTable First Research Shape.",
362
+ `Handle: ${shape.handle}`,
363
+ `Goal: ${shape.currentGoal}`,
364
+ shape.currentBlocker ? `Blocker: ${shape.currentBlocker}` : undefined,
365
+ `Next action: ${shape.nextAction}`
366
+ ].filter(Boolean).join("\n"),
367
+ title: "First Research Shape",
368
+ question: "How should LongTable treat this first research handle?",
369
+ checkpointKey: "first_research_shape_confirmation",
370
+ displayReason: "This is the first structured handoff from open interview to durable project state.",
371
+ options: [
372
+ { value: "confirm", label: "Confirm this handle", description: "Use this as the provisional research handle.", recommended: true },
373
+ { value: "revise", label: "Revise the handle", description: "Keep interviewing before recording this shape." },
374
+ { value: "gather_context", label: "Gather more context", description: "Ask for one more scene, case, source, or material first." },
375
+ { value: "defer", label: "Keep it open", description: "Do not treat the research handle as ready yet." }
376
+ ]
377
+ };
378
+ }
379
+ async function markFirstResearchShapeConfirmation(context, shape, answer, questionId, decisionId) {
380
+ const state = asInterviewState(await loadWorkspaceState(context));
381
+ const timestamp = new Date().toISOString();
382
+ const confirmedShape = answer === "confirm"
383
+ ? { ...shape, confirmedAt: timestamp }
384
+ : shape;
385
+ state.firstResearchShape = confirmedShape;
386
+ state.workingState = {
387
+ ...state.workingState,
388
+ firstResearchShape: confirmedShape
389
+ };
390
+ state.hooks = (state.hooks ?? []).map((hook) => {
391
+ if (hook.id !== shape.sourceHookId) {
392
+ return hook;
393
+ }
394
+ return {
395
+ ...hook,
396
+ status: answer === "confirm" ? "confirmed" : answer === "defer" ? "deferred" : "active",
397
+ updatedAt: timestamp,
398
+ firstResearchShape: confirmedShape,
399
+ linkedQuestionRecordIds: questionId
400
+ ? [...(hook.linkedQuestionRecordIds ?? []), questionId]
401
+ : hook.linkedQuestionRecordIds,
402
+ linkedDecisionRecordIds: decisionId
403
+ ? [...(hook.linkedDecisionRecordIds ?? []), decisionId]
404
+ : hook.linkedDecisionRecordIds
405
+ };
406
+ });
407
+ const session = {
408
+ ...context.session,
409
+ firstResearchShape: confirmedShape,
410
+ lastUpdatedAt: timestamp
411
+ };
412
+ context.session = session;
413
+ await writeFile(context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
414
+ await writeFile(context.stateFilePath, JSON.stringify(state, null, 2), "utf8");
415
+ await syncCurrentWorkspaceView(context);
416
+ return { state, session, shape: confirmedShape };
417
+ }
151
418
  function statusForElicitationError(error) {
152
419
  const message = error instanceof Error ? error.message : String(error);
153
420
  if (/timed?\s*out|timeout/i.test(message)) {
@@ -238,6 +505,214 @@ export function createLongTableMcpServer() {
238
505
  return errorResult(error instanceof Error ? error.message : String(error));
239
506
  }
240
507
  });
508
+ server.registerTool("create_workspace", {
509
+ title: "Create LongTable Workspace",
510
+ description: "Create a .longtable workspace in the current folder for provider-native $longtable-interview.",
511
+ inputSchema: cwdSchema.extend({
512
+ projectName: z.string().optional(),
513
+ projectPath: z.string().optional(),
514
+ seedGoal: z.string().optional(),
515
+ setupPath: z.string().optional(),
516
+ provider: z.enum(["codex", "claude"]).default("codex")
517
+ })
518
+ }, async ({ cwd: inputCwd, projectName, projectPath, seedGoal, setupPath, provider }) => {
519
+ try {
520
+ const targetPath = resolveStartPath(projectPath ?? inputCwd);
521
+ const setup = await loadSetupOutput(setupPath);
522
+ const context = await createOrUpdateProjectWorkspace({
523
+ projectName: projectName?.trim() || basename(targetPath) || "LongTable Research",
524
+ projectPath: targetPath,
525
+ currentGoal: seedGoal?.trim() || "First research handle pending",
526
+ requestedPerspectives: [],
527
+ disagreementPreference: setup.profileSeed.panelPreference ?? "show_on_conflict",
528
+ setup
529
+ });
530
+ const interview = await beginInterviewHook(context, {
531
+ provider: provider,
532
+ openingQuestion: "What do you want to research? If it is not clear yet, describe the problem in its rough form.",
533
+ seedAnswer: seedGoal
534
+ });
535
+ return textResult({
536
+ project: context.project,
537
+ session: context.session,
538
+ hook: interview.hook,
539
+ files: {
540
+ project: context.projectFilePath,
541
+ session: context.sessionFilePath,
542
+ state: context.stateFilePath,
543
+ current: context.currentFilePath
544
+ },
545
+ nextQuestion: "What do you want to research? If it is not clear yet, describe the problem in its rough form."
546
+ });
547
+ }
548
+ catch (error) {
549
+ return errorResult(error instanceof Error ? error.message : String(error));
550
+ }
551
+ });
552
+ server.registerTool("begin_interview", {
553
+ title: "Begin LongTable Interview",
554
+ description: "Create or resume the active $longtable-interview hook in an existing workspace.",
555
+ inputSchema: cwdSchema.extend({
556
+ openingQuestion: z.string().optional(),
557
+ seedAnswer: z.string().optional(),
558
+ provider: z.enum(["codex", "claude"]).default("codex")
559
+ })
560
+ }, async ({ cwd: inputCwd, openingQuestion, seedAnswer, provider }) => {
561
+ try {
562
+ const context = await requireContext(inputCwd);
563
+ const result = await beginInterviewHook(context, {
564
+ provider: provider,
565
+ openingQuestion,
566
+ seedAnswer
567
+ });
568
+ return textResult(result);
569
+ }
570
+ catch (error) {
571
+ return errorResult(error instanceof Error ? error.message : String(error));
572
+ }
573
+ });
574
+ server.registerTool("append_interview_turn", {
575
+ title: "Append LongTable Interview Turn",
576
+ description: "Record one natural-language interview question and answer, including quality/follow-up metadata.",
577
+ inputSchema: cwdSchema.extend({
578
+ hookId: z.string().optional(),
579
+ question: z.string().min(1),
580
+ answer: z.string().min(1),
581
+ reflection: z.string().optional(),
582
+ quality: z.enum(["thin", "usable", "rich"]).optional(),
583
+ needsFollowUp: z.boolean().optional(),
584
+ followUpQuestion: z.string().optional(),
585
+ rationale: z.array(z.string()).optional()
586
+ })
587
+ }, async ({ cwd: inputCwd, hookId, question, answer, reflection, quality, needsFollowUp, followUpQuestion, rationale }) => {
588
+ try {
589
+ const context = await requireContext(inputCwd);
590
+ const result = await appendInterviewTurn(context, {
591
+ hookId,
592
+ question,
593
+ answer,
594
+ reflection,
595
+ quality: quality,
596
+ needsFollowUp,
597
+ followUpQuestion,
598
+ rationale
599
+ });
600
+ return textResult(result);
601
+ }
602
+ catch (error) {
603
+ return errorResult(error instanceof Error ? error.message : String(error));
604
+ }
605
+ });
606
+ server.registerTool("summarize_interview", {
607
+ title: "Summarize LongTable Interview",
608
+ description: "Store the provisional First Research Shape after enough interview context has accumulated.",
609
+ inputSchema: cwdSchema.extend({
610
+ hookId: z.string().optional(),
611
+ shape: firstResearchShapeSchema
612
+ })
613
+ }, async ({ cwd: inputCwd, hookId, shape }) => {
614
+ try {
615
+ const context = await requireContext(inputCwd);
616
+ const result = await summarizeInterviewHook(context, {
617
+ hookId,
618
+ shape: shape
619
+ });
620
+ return textResult(result);
621
+ }
622
+ catch (error) {
623
+ return errorResult(error instanceof Error ? error.message : String(error));
624
+ }
625
+ });
626
+ server.registerTool("confirm_first_research_shape", {
627
+ title: "Confirm First Research Shape",
628
+ description: "Use MCP form elicitation to confirm, revise, defer, or request more context for the First Research Shape.",
629
+ inputSchema: cwdSchema.extend({
630
+ shape: firstResearchShapeSchema.optional(),
631
+ provider: z.enum(["codex", "claude"]).default("codex"),
632
+ fallbackOnly: z.boolean().default(false)
633
+ })
634
+ }, async ({ cwd: inputCwd, shape: inputShape, provider, fallbackOnly }) => {
635
+ try {
636
+ const context = await requireContext(inputCwd);
637
+ const state = asInterviewState(await loadWorkspaceState(context));
638
+ const shape = inputShape ?? state.firstResearchShape;
639
+ if (!shape) {
640
+ return errorResult("No First Research Shape was found to confirm. Run summarize_interview first.");
641
+ }
642
+ const spec = buildFirstResearchShapeQuestion(shape);
643
+ const created = await createWorkspaceQuestion({
644
+ context,
645
+ prompt: spec.prompt,
646
+ title: spec.title,
647
+ question: spec.question,
648
+ checkpointKey: spec.checkpointKey,
649
+ questionOptions: spec.options,
650
+ displayReason: spec.displayReason,
651
+ provider: provider,
652
+ required: true
653
+ });
654
+ const fallback = renderQuestionFallback(created.question, provider);
655
+ if (fallbackOnly) {
656
+ const marked = await markQuestionTransport(context, created.question.id, "fallback_rendered", "MCP elicitation skipped by fallbackOnly.");
657
+ return textResult({
658
+ question: marked ?? created.question,
659
+ shape,
660
+ elicitation: { attempted: false, reason: "fallbackOnly" },
661
+ fallback
662
+ });
663
+ }
664
+ try {
665
+ await markQuestionTransport(context, created.question.id, "attempted");
666
+ const elicited = await server.server.elicitInput(buildElicitationParams(created.question));
667
+ const accepted = acceptedAnswer(elicited);
668
+ if (!accepted) {
669
+ const status = elicited.action === "decline" || elicited.action === "cancel"
670
+ ? "declined"
671
+ : "fallback_rendered";
672
+ const marked = await markQuestionTransport(context, created.question.id, status, `MCP elicitation returned action: ${elicited.action}.`);
673
+ return textResult({
674
+ question: marked ?? created.question,
675
+ shape,
676
+ elicitation: { attempted: true, action: elicited.action },
677
+ fallback
678
+ });
679
+ }
680
+ const decided = await answerWorkspaceQuestion({
681
+ context,
682
+ questionId: created.question.id,
683
+ answer: accepted.answer,
684
+ provider: provider,
685
+ surface: "mcp_elicitation"
686
+ });
687
+ const marked = await markQuestionTransport(context, created.question.id, "accepted");
688
+ const confirmation = await markFirstResearchShapeConfirmation(context, shape, accepted.answer, created.question.id, decided.decision.id);
689
+ return textResult({
690
+ shape: confirmation.shape,
691
+ question: marked ? { ...decided.question, transportStatus: marked.transportStatus } : decided.question,
692
+ decision: decided.decision,
693
+ elicitation: { attempted: true, action: elicited.action }
694
+ });
695
+ }
696
+ catch (elicitationError) {
697
+ const status = statusForElicitationError(elicitationError);
698
+ const message = elicitationError instanceof Error ? elicitationError.message : String(elicitationError);
699
+ const marked = await markQuestionTransport(context, created.question.id, status, message);
700
+ return textResult({
701
+ question: marked ?? created.question,
702
+ shape,
703
+ elicitation: {
704
+ attempted: true,
705
+ supported: status !== "unsupported" ? undefined : false,
706
+ error: message
707
+ },
708
+ fallback
709
+ });
710
+ }
711
+ }
712
+ catch (error) {
713
+ return errorResult(error instanceof Error ? error.message : String(error));
714
+ }
715
+ });
241
716
  server.registerTool("pending_questions", {
242
717
  title: "List Pending Researcher Checkpoints",
243
718
  description: "List pending LongTable QuestionRecords.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/mcp",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
4
4
  "private": false,
5
5
  "description": "LongTable MCP transport for workspace state and Researcher Checkpoints",
6
6
  "type": "module",
@@ -26,11 +26,12 @@
26
26
  "self-test": "node ./dist/server.js --self-test"
27
27
  },
28
28
  "dependencies": {
29
- "@longtable/checkpoints": "0.1.31",
30
- "@longtable/cli": "0.1.31",
31
- "@longtable/core": "0.1.31",
32
- "@longtable/provider-claude": "0.1.31",
33
- "@longtable/provider-codex": "0.1.31",
29
+ "@longtable/checkpoints": "0.1.32",
30
+ "@longtable/cli": "0.1.32",
31
+ "@longtable/core": "0.1.32",
32
+ "@longtable/provider-claude": "0.1.32",
33
+ "@longtable/provider-codex": "0.1.32",
34
+ "@longtable/setup": "0.1.32",
34
35
  "@modelcontextprotocol/sdk": "^1.29.0",
35
36
  "zod": "^4.0.0"
36
37
  },