@longtable/mcp 0.1.53 → 0.1.54

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 (2) hide show
  1. package/dist/server.js +104 -22
  2. package/package.json +7 -7
package/dist/server.js CHANGED
@@ -10,6 +10,7 @@ import { z } from "zod";
10
10
  import { classifyCheckpointTrigger } from "@longtable/checkpoints";
11
11
  import { renderQuestionRecordInput } from "@longtable/provider-claude";
12
12
  import { renderQuestionRecordPrompt } from "@longtable/provider-codex";
13
+ import { evaluateResearchSpecificationReadiness } from "@longtable/core";
13
14
  import { loadSetupOutput } from "@longtable/setup";
14
15
  import { answerWorkspaceQuestion, applyResearchSpecificationAuditUpdate, applyResearchSpecificationPatch, clearWorkspaceQuestion, createOrUpdateProjectWorkspace, createWorkspaceQuestion, diffResearchSpecifications, findUnincorporatedResearchEvidence, inspectProjectWorkspace, loadProjectContextFromDirectory, loadWorkspaceState, proposeResearchSpecificationPatch, readResearchSpecificationHistory, syncCurrentWorkspaceView } from "@longtable/cli";
15
16
  import { buildFirstResearchShapeQuestion, firstResearchShapeAnswerConfirms, firstResearchShapeAnswerStatus } from "./first-research-shape.js";
@@ -196,6 +197,36 @@ function createId(prefix) {
196
197
  function asInterviewState(state) {
197
198
  return state;
198
199
  }
200
+ function researchSpecificationReadinessFromState(state, session, specification) {
201
+ return evaluateResearchSpecificationReadiness({
202
+ firstResearchShape: state.firstResearchShape ?? session?.firstResearchShape,
203
+ researchSpecification: specification ?? state.researchSpecification ?? session?.researchSpecification,
204
+ questionLog: state.questionLog,
205
+ questionObligations: state.questionObligations
206
+ });
207
+ }
208
+ function nextStartQuestionForReadiness(readiness, openingQuestion) {
209
+ if (readiness.status === "shape_only") {
210
+ return "A First Research Shape is saved, but the Research Specification is still missing. Continue with the next Research Specification question: what research question, construct boundary, evidence boundary, or method commitment must be preserved before follow-up interview starts?";
211
+ }
212
+ if (readiness.status === "structurally_incomplete") {
213
+ return `The Research Specification draft is missing: ${readiness.blockingGaps.join(", ")}. Ask the next natural-language question that resolves the highest-risk missing section.`;
214
+ }
215
+ if (readiness.status === "draft_pending_confirmation") {
216
+ return "A Research Specification draft is saved. Show the Research Specification Preview and run confirm_research_specification before moving to option-first follow-up interview.";
217
+ }
218
+ if (readiness.status === "deferred") {
219
+ return "Research Specification confirmation is deferred. Resume the confirmation checkpoint before moving to option-first follow-up interview.";
220
+ }
221
+ if (readiness.status === "confirmed") {
222
+ return "The Research Specification is confirmed. Use $longtable-interview for structured follow-up decisions.";
223
+ }
224
+ return openingQuestion ?? "What do you want to research? If it is not clear yet, describe the problem in its rough form.";
225
+ }
226
+ async function currentResearchSpecificationReadiness(context, specification) {
227
+ const state = asInterviewState(await loadWorkspaceState(context));
228
+ return researchSpecificationReadinessFromState(state, context.session, specification);
229
+ }
199
230
  function isInterviewHookRun(hook) {
200
231
  return hook?.kind === "longtable_interview";
201
232
  }
@@ -351,21 +382,14 @@ function defaultFollowUpQuestion(answer) {
351
382
  }
352
383
  async function beginInterviewHook(context, options) {
353
384
  const state = asInterviewState(await loadWorkspaceState(context));
385
+ const readiness = researchSpecificationReadinessFromState(state, context.session);
354
386
  const existing = activeInterviewHook(state);
355
387
  if (existing) {
356
- return { hook: existing, state };
357
- }
358
- const confirmedShape = state.firstResearchShape?.confirmedAt ? state.firstResearchShape : undefined;
359
- if (confirmedShape) {
360
- const confirmedHook = [...(state.hooks ?? [])].reverse().find((hook) => isInterviewHookRun(hook) &&
361
- hook.status === "confirmed" &&
362
- hook.firstResearchShape?.handle === confirmedShape.handle);
363
388
  return {
364
- hook: confirmedHook,
365
389
  state,
366
- alreadyConfirmed: true,
367
- shape: confirmedShape,
368
- nextQuestion: options.openingQuestion ?? confirmedShape.openQuestions[0] ?? confirmedShape.nextAction
390
+ hook: existing,
391
+ readiness,
392
+ nextQuestion: nextStartQuestionForReadiness(readiness, options.openingQuestion)
369
393
  };
370
394
  }
371
395
  const timestamp = new Date().toISOString();
@@ -375,13 +399,16 @@ async function beginInterviewHook(context, options) {
375
399
  status: "active",
376
400
  createdAt: timestamp,
377
401
  updatedAt: timestamp,
378
- targetOutcome: "first_research_handle",
402
+ targetOutcome: readiness.status === "no_spec" ? "first_research_handle" : "research_specification",
379
403
  depth: "gathering_context",
380
404
  provider: options.provider,
381
405
  turns: [],
382
406
  qualityNotes: [],
383
407
  rationale: [
384
- "Official LongTable research start surface is provider-native `$longtable-start`, not the CLI start questionnaire."
408
+ "Official LongTable research start surface is provider-native `$longtable-start`, not the CLI start questionnaire.",
409
+ ...(readiness.status === "shape_only"
410
+ ? ["Confirmed First Research Shape is not enough; continue into Research Specification before follow-up interview."]
411
+ : [])
385
412
  ]
386
413
  };
387
414
  const updated = upsertInterviewHook(state, hook);
@@ -394,7 +421,12 @@ async function beginInterviewHook(context, options) {
394
421
  };
395
422
  await writeFile(context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
396
423
  await syncCurrentWorkspaceView(context);
397
- return { hook, state: updated };
424
+ return {
425
+ hook,
426
+ state: updated,
427
+ readiness,
428
+ nextQuestion: nextStartQuestionForReadiness(readiness, options.openingQuestion)
429
+ };
398
430
  }
399
431
  async function appendInterviewTurn(context, options) {
400
432
  const state = asInterviewState(await loadWorkspaceState(context));
@@ -511,7 +543,13 @@ async function summarizeInterviewHook(context, options) {
511
543
  await writeFile(context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
512
544
  await writeFile(context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
513
545
  await syncCurrentWorkspaceView(context);
514
- return { hook, shape, state: updated, session };
546
+ return {
547
+ hook,
548
+ shape,
549
+ state: updated,
550
+ session,
551
+ readiness: researchSpecificationReadinessFromState(updated, session)
552
+ };
515
553
  }
516
554
  function normalizeStringArray(values) {
517
555
  return (values ?? []).map((value) => value.trim()).filter(Boolean);
@@ -624,7 +662,13 @@ async function summarizeResearchSpecificationHook(context, options) {
624
662
  await writeFile(context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
625
663
  await writeFile(context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
626
664
  await syncCurrentWorkspaceView(context);
627
- return { hook, specification, state: updated, session };
665
+ return {
666
+ hook,
667
+ specification,
668
+ state: updated,
669
+ session,
670
+ readiness: researchSpecificationReadinessFromState(updated, session, specification)
671
+ };
628
672
  }
629
673
  async function cancelInterviewHook(context, options) {
630
674
  const state = asInterviewState(await loadWorkspaceState(context));
@@ -855,7 +899,12 @@ async function markFirstResearchShapeConfirmation(context, shape, answer, questi
855
899
  await writeFile(context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
856
900
  await writeFile(context.stateFilePath, JSON.stringify(nextState, null, 2), "utf8");
857
901
  await syncCurrentWorkspaceView(context);
858
- return { state: nextState, session, shape: confirmedShape };
902
+ return {
903
+ state: nextState,
904
+ session,
905
+ shape: confirmedShape,
906
+ readiness: researchSpecificationReadinessFromState(nextState, session)
907
+ };
859
908
  }
860
909
  async function markAlreadyConfirmedFirstResearchShape(context, shape) {
861
910
  const state = asInterviewState(await loadWorkspaceState(context));
@@ -895,7 +944,12 @@ async function markAlreadyConfirmedFirstResearchShape(context, shape) {
895
944
  await writeFile(context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
896
945
  await writeFile(context.stateFilePath, JSON.stringify(nextState, null, 2), "utf8");
897
946
  await syncCurrentWorkspaceView(context);
898
- return { state: nextState, session, shape: confirmedShape };
947
+ return {
948
+ state: nextState,
949
+ session,
950
+ shape: confirmedShape,
951
+ readiness: researchSpecificationReadinessFromState(nextState, session)
952
+ };
899
953
  }
900
954
  async function markResearchSpecificationConfirmation(context, specification, answer, questionId, decisionId) {
901
955
  const state = asInterviewState(await loadWorkspaceState(context));
@@ -964,7 +1018,12 @@ async function markResearchSpecificationConfirmation(context, specification, ans
964
1018
  await writeFile(context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
965
1019
  await writeFile(context.stateFilePath, JSON.stringify(nextState, null, 2), "utf8");
966
1020
  await syncCurrentWorkspaceView(context);
967
- return { state: nextState, session, specification: confirmedSpecification };
1021
+ return {
1022
+ state: nextState,
1023
+ session,
1024
+ specification: confirmedSpecification,
1025
+ readiness: researchSpecificationReadinessFromState(nextState, session, confirmedSpecification)
1026
+ };
968
1027
  }
969
1028
  async function markAlreadyConfirmedResearchSpecification(context, specification) {
970
1029
  const state = asInterviewState(await loadWorkspaceState(context));
@@ -1005,7 +1064,12 @@ async function markAlreadyConfirmedResearchSpecification(context, specification)
1005
1064
  await writeFile(context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
1006
1065
  await writeFile(context.stateFilePath, JSON.stringify(nextState, null, 2), "utf8");
1007
1066
  await syncCurrentWorkspaceView(context);
1008
- return { state: nextState, session, specification: confirmedSpecification };
1067
+ return {
1068
+ state: nextState,
1069
+ session,
1070
+ specification: confirmedSpecification,
1071
+ readiness: researchSpecificationReadinessFromState(nextState, session, confirmedSpecification)
1072
+ };
1009
1073
  }
1010
1074
  function statusForElicitationError(error) {
1011
1075
  const message = error instanceof Error ? error.message : String(error);
@@ -1134,7 +1198,10 @@ export function createLongTableMcpServer() {
1134
1198
  state: context.stateFilePath,
1135
1199
  current: context.currentFilePath
1136
1200
  },
1137
- nextQuestion: "What do you want to research? If it is not clear yet, describe the problem in its rough form."
1201
+ readiness: "readiness" in interview ? interview.readiness : undefined,
1202
+ nextQuestion: "nextQuestion" in interview
1203
+ ? interview.nextQuestion
1204
+ : "What do you want to research? If it is not clear yet, describe the problem in its rough form."
1138
1205
  });
1139
1206
  }
1140
1207
  catch (error) {
@@ -1161,6 +1228,7 @@ export function createLongTableMcpServer() {
1161
1228
  hook: compactInterviewHook(result.hook),
1162
1229
  alreadyConfirmed: "alreadyConfirmed" in result ? Boolean(result.alreadyConfirmed) : false,
1163
1230
  shape: "shape" in result ? result.shape : result.hook?.firstResearchShape,
1231
+ readiness: "readiness" in result ? result.readiness : undefined,
1164
1232
  nextQuestion: "nextQuestion" in result ? result.nextQuestion : openingQuestion,
1165
1233
  workspace: {
1166
1234
  projectName: context.project.projectName,
@@ -1227,6 +1295,7 @@ export function createLongTableMcpServer() {
1227
1295
  return textResult({
1228
1296
  hook: compactInterviewHook(result.hook),
1229
1297
  shape: result.shape,
1298
+ readiness: result.readiness,
1230
1299
  session: {
1231
1300
  currentGoal: result.session.currentGoal,
1232
1301
  currentBlocker: result.session.currentBlocker,
@@ -1257,6 +1326,7 @@ export function createLongTableMcpServer() {
1257
1326
  hook: compactInterviewHook(result.hook),
1258
1327
  specification: result.specification,
1259
1328
  preview: renderResearchSpecificationPreview(result.specification),
1329
+ readiness: result.readiness,
1260
1330
  session: {
1261
1331
  currentGoal: result.session.currentGoal,
1262
1332
  currentBlocker: result.session.currentBlocker,
@@ -1283,13 +1353,15 @@ export function createLongTableMcpServer() {
1283
1353
  if (!specification) {
1284
1354
  return textResult({
1285
1355
  found: false,
1356
+ readiness: researchSpecificationReadinessFromState(state, session),
1286
1357
  message: "No Research Specification was found. Run summarize_research_specification first."
1287
1358
  });
1288
1359
  }
1289
1360
  return textResult({
1290
1361
  found: true,
1291
1362
  specification,
1292
- preview: renderResearchSpecificationPreview(specification)
1363
+ preview: renderResearchSpecificationPreview(specification),
1364
+ readiness: researchSpecificationReadinessFromState(state, session, specification)
1293
1365
  });
1294
1366
  }
1295
1367
  catch (error) {
@@ -1456,6 +1528,7 @@ export function createLongTableMcpServer() {
1456
1528
  const confirmation = await markAlreadyConfirmedFirstResearchShape(context, shape);
1457
1529
  return textResult({
1458
1530
  shape: confirmation.shape,
1531
+ readiness: confirmation.readiness,
1459
1532
  elicitation: { attempted: false, reason: "already_confirmed" }
1460
1533
  });
1461
1534
  }
@@ -1484,6 +1557,7 @@ export function createLongTableMcpServer() {
1484
1557
  return textResult({
1485
1558
  question: marked ?? created.question,
1486
1559
  shape,
1560
+ readiness: await currentResearchSpecificationReadiness(context),
1487
1561
  elicitation: { attempted: false, reason: "fallbackOnly" },
1488
1562
  fallback
1489
1563
  });
@@ -1507,6 +1581,7 @@ export function createLongTableMcpServer() {
1507
1581
  return textResult({
1508
1582
  question: cleared?.question ?? marked ?? created.question,
1509
1583
  shape,
1584
+ readiness: await currentResearchSpecificationReadiness(context),
1510
1585
  elicitation: { attempted: true, action: elicited.action },
1511
1586
  fallback
1512
1587
  });
@@ -1522,6 +1597,7 @@ export function createLongTableMcpServer() {
1522
1597
  const confirmation = await markFirstResearchShapeConfirmation(context, shape, firstAcceptedAnswerValue(accepted.answer), created.question.id, decided.decision.id);
1523
1598
  return textResult({
1524
1599
  shape: confirmation.shape,
1600
+ readiness: confirmation.readiness,
1525
1601
  question: marked ? { ...decided.question, transportStatus: marked.transportStatus } : decided.question,
1526
1602
  decision: decided.decision,
1527
1603
  elicitation: { attempted: true, action: elicited.action }
@@ -1534,6 +1610,7 @@ export function createLongTableMcpServer() {
1534
1610
  return textResult({
1535
1611
  question: marked ?? created.question,
1536
1612
  shape,
1613
+ readiness: await currentResearchSpecificationReadiness(context),
1537
1614
  elicitation: {
1538
1615
  attempted: true,
1539
1616
  supported: status !== "unsupported" ? undefined : false,
@@ -1571,6 +1648,7 @@ export function createLongTableMcpServer() {
1571
1648
  return textResult({
1572
1649
  specification: confirmation.specification,
1573
1650
  preview: renderResearchSpecificationPreview(confirmation.specification),
1651
+ readiness: confirmation.readiness,
1574
1652
  elicitation: { attempted: false, reason: "already_confirmed" }
1575
1653
  });
1576
1654
  }
@@ -1593,6 +1671,7 @@ export function createLongTableMcpServer() {
1593
1671
  question: marked ?? created.question,
1594
1672
  specification,
1595
1673
  preview: renderResearchSpecificationPreview(specification),
1674
+ readiness: await currentResearchSpecificationReadiness(context, specification),
1596
1675
  elicitation: { attempted: false, reason: "fallbackOnly" },
1597
1676
  fallback
1598
1677
  });
@@ -1617,6 +1696,7 @@ export function createLongTableMcpServer() {
1617
1696
  question: cleared?.question ?? marked ?? created.question,
1618
1697
  specification,
1619
1698
  preview: renderResearchSpecificationPreview(specification),
1699
+ readiness: await currentResearchSpecificationReadiness(context, specification),
1620
1700
  elicitation: { attempted: true, action: elicited.action },
1621
1701
  fallback
1622
1702
  });
@@ -1633,6 +1713,7 @@ export function createLongTableMcpServer() {
1633
1713
  return textResult({
1634
1714
  specification: confirmation.specification,
1635
1715
  preview: renderResearchSpecificationPreview(confirmation.specification),
1716
+ readiness: confirmation.readiness,
1636
1717
  question: marked ? { ...decided.question, transportStatus: marked.transportStatus } : decided.question,
1637
1718
  decision: decided.decision,
1638
1719
  elicitation: { attempted: true, action: elicited.action }
@@ -1646,6 +1727,7 @@ export function createLongTableMcpServer() {
1646
1727
  question: marked ?? created.question,
1647
1728
  specification,
1648
1729
  preview: renderResearchSpecificationPreview(specification),
1730
+ readiness: await currentResearchSpecificationReadiness(context, specification),
1649
1731
  elicitation: {
1650
1732
  attempted: true,
1651
1733
  supported: status !== "unsupported" ? undefined : false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/mcp",
3
- "version": "0.1.53",
3
+ "version": "0.1.54",
4
4
  "private": false,
5
5
  "description": "LongTable MCP transport for workspace state and Researcher Checkpoints",
6
6
  "type": "module",
@@ -26,12 +26,12 @@
26
26
  "self-test": "node ./dist/server.js --self-test"
27
27
  },
28
28
  "dependencies": {
29
- "@longtable/checkpoints": "0.1.53",
30
- "@longtable/cli": "0.1.53",
31
- "@longtable/core": "0.1.53",
32
- "@longtable/provider-claude": "0.1.53",
33
- "@longtable/provider-codex": "0.1.53",
34
- "@longtable/setup": "0.1.53",
29
+ "@longtable/checkpoints": "0.1.54",
30
+ "@longtable/cli": "0.1.54",
31
+ "@longtable/core": "0.1.54",
32
+ "@longtable/provider-claude": "0.1.54",
33
+ "@longtable/provider-codex": "0.1.54",
34
+ "@longtable/setup": "0.1.54",
35
35
  "@modelcontextprotocol/sdk": "^1.29.0",
36
36
  "zod": "^4.0.0"
37
37
  },