@longtable/mcp 0.1.40 → 0.1.42

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.
package/README.md CHANGED
@@ -15,7 +15,7 @@ longtable-state
15
15
  Run:
16
16
 
17
17
  ```bash
18
- npx -y @longtable/mcp@0.1.40
18
+ npx -y @longtable/mcp@0.1.41
19
19
  ```
20
20
 
21
21
  Self-test:
@@ -39,6 +39,7 @@ Provider guidance should use interview tools for `$longtable-interview`:
39
39
  - `begin_interview`
40
40
  - `append_interview_turn`
41
41
  - `summarize_interview`
42
+ - `cancel_interview`
42
43
  - `confirm_first_research_shape`
43
44
 
44
45
  For later Researcher Checkpoints, provider guidance should use
@@ -1,4 +1,7 @@
1
1
  function questionTitle(shape) {
2
+ if (usesKorean(shape)) {
3
+ return "First Research Shape 확인";
4
+ }
2
5
  if (shape.protectedDecision) {
3
6
  return "Protected Research Decision";
4
7
  }
@@ -7,7 +10,22 @@ function questionTitle(shape) {
7
10
  }
8
11
  return "Emerging Research Direction";
9
12
  }
13
+ function usesKorean(shape) {
14
+ return [
15
+ shape.handle,
16
+ shape.currentGoal,
17
+ shape.currentBlocker,
18
+ shape.researchObject,
19
+ shape.gapRisk,
20
+ shape.protectedDecision,
21
+ ...shape.openQuestions,
22
+ shape.nextAction
23
+ ].some((value) => typeof value === "string" && /[ㄱ-ㅎㅏ-ㅣ가-힣]/.test(value));
24
+ }
10
25
  function questionText(shape) {
26
+ if (usesKorean(shape)) {
27
+ return "이 First Research Shape를 어떻게 처리할까요?";
28
+ }
11
29
  if (shape.protectedDecision) {
12
30
  return "Before LongTable moves forward, what protected research decision should stay explicit?";
13
31
  }
@@ -20,6 +38,18 @@ function questionText(shape) {
20
38
  return "How should LongTable carry this emerging study forward?";
21
39
  }
22
40
  function displayReason(shape) {
41
+ if (usesKorean(shape)) {
42
+ if (shape.protectedDecision) {
43
+ return `인터뷰에서 보호해야 할 연구 판단이 드러났습니다: ${shape.protectedDecision}. LongTable은 이 판단이 조용히 확정되지 않게 해야 합니다.`;
44
+ }
45
+ if (shape.currentBlocker) {
46
+ return `주요 blocker가 아직 열려 있습니다: ${shape.currentBlocker}. 다음 행동은 이 불확실성을 명시적으로 다뤄야 합니다.`;
47
+ }
48
+ if (shape.openQuestions.length > 0) {
49
+ return "인터뷰에서 열린 연구 질문이 드러났습니다. 가장 중요한 질문을 보존하는 다음 행동을 선택해야 합니다.";
50
+ }
51
+ return "LongTable은 provisional research direction을 제안할 만큼의 맥락을 얻었지만, 다음 연구 행동은 명시적으로 선택되어야 합니다.";
52
+ }
23
53
  if (shape.protectedDecision) {
24
54
  return `The interview surfaced a protected decision: ${shape.protectedDecision}. LongTable should not let it settle silently.`;
25
55
  }
@@ -46,47 +76,59 @@ function recommendedValue(shape) {
46
76
  function baseOptions(shape) {
47
77
  const recommended = recommendedValue(shape);
48
78
  const options = [];
79
+ const korean = usesKorean(shape);
49
80
  if (shape.protectedDecision) {
50
81
  options.push({
51
82
  value: "protect_decision",
52
- label: "Keep the protected decision open",
53
- description: `Treat this as the guarded judgment while the broader direction stays provisional: ${shape.protectedDecision}`,
83
+ label: korean ? "보호 결정 열어두기" : "Keep the protected decision open",
84
+ description: korean
85
+ ? `전체 방향은 provisional로 두고, 이 판단을 보호된 결정으로 유지합니다: ${shape.protectedDecision}`
86
+ : `Treat this as the guarded judgment while the broader direction stays provisional: ${shape.protectedDecision}`,
54
87
  recommended: recommended === "protect_decision"
55
88
  });
56
89
  }
57
90
  options.push({
58
91
  value: "stabilize_shape",
59
- label: "Use this as the provisional direction",
60
- description: "Carry this research shape forward as the current working direction.",
92
+ label: korean ? "저장/확정" : "Use this as the provisional direction",
93
+ description: korean
94
+ ? "이 research shape를 현재 작업 방향으로 저장하고 이어갑니다."
95
+ : "Carry this research shape forward as the current working direction.",
61
96
  recommended: recommended === "stabilize_shape"
62
97
  }, {
63
98
  value: "gather_context",
64
- label: "Gather one more concrete case",
65
- description: "Ask for one more scene, source, dataset, or example before stabilizing it.",
99
+ label: korean ? "한 질문 더" : "Gather one more concrete case",
100
+ description: korean
101
+ ? "저장하기 전에 한 가지 장면, 출처, 데이터셋, 사례, 또는 결정 맥락을 더 묻습니다."
102
+ : "Ask for one more scene, source, dataset, or example before stabilizing it.",
66
103
  recommended: recommended === "gather_context"
67
104
  }, {
68
105
  value: "revise_shape",
69
- label: "Revise the emerging shape",
70
- description: "Change the handle, blocker, or next action before LongTable treats it as usable."
106
+ label: korean ? "수정" : "Revise the emerging shape",
107
+ description: korean
108
+ ? "LongTable이 이를 usable한 방향으로 다루기 전에 handle, blocker, next action 등을 수정합니다."
109
+ : "Change the handle, blocker, or next action before LongTable treats it as usable."
71
110
  });
72
111
  if (!shape.protectedDecision) {
73
112
  options.push({
74
113
  value: "keep_open",
75
- label: "Keep the study open longer",
76
- description: "Do not stabilize this direction yet."
114
+ label: korean ? "열어두기" : "Keep the study open longer",
115
+ description: korean ? "아직 이 방향을 확정하지 않습니다." : "Do not stabilize this direction yet."
77
116
  });
78
117
  }
79
118
  return options;
80
119
  }
81
120
  export function buildFirstResearchShapeQuestion(shape) {
121
+ const korean = usesKorean(shape);
82
122
  const summaryLines = [
83
- "LongTable has enough context to propose a provisional research direction.",
84
- `Handle: ${shape.handle}`,
85
- `Goal: ${shape.currentGoal}`,
86
- shape.currentBlocker ? `Blocker: ${shape.currentBlocker}` : undefined,
87
- shape.protectedDecision ? `Protected decision: ${shape.protectedDecision}` : undefined,
88
- shape.openQuestions.length > 0 ? `Open question: ${shape.openQuestions[0]}` : undefined,
89
- `Next action: ${shape.nextAction}`
123
+ korean
124
+ ? "LongTable이 provisional research direction을 제안할 만큼의 맥락을 얻었습니다."
125
+ : "LongTable has enough context to propose a provisional research direction.",
126
+ `${korean ? "핸들" : "Handle"}: ${shape.handle}`,
127
+ `${korean ? "목표" : "Goal"}: ${shape.currentGoal}`,
128
+ shape.currentBlocker ? `${korean ? "막힌 지점" : "Blocker"}: ${shape.currentBlocker}` : undefined,
129
+ shape.protectedDecision ? `${korean ? "보호할 결정" : "Protected decision"}: ${shape.protectedDecision}` : undefined,
130
+ shape.openQuestions.length > 0 ? `${korean ? "열린 질문" : "Open question"}: ${shape.openQuestions[0]}` : undefined,
131
+ `${korean ? "다음 행동" : "Next action"}: ${shape.nextAction}`
90
132
  ].filter(Boolean);
91
133
  return {
92
134
  prompt: summaryLines.join("\n"),
package/dist/server.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readFile, writeFile } from "node:fs/promises";
2
2
  import { existsSync } from "node:fs";
3
+ import { createRequire } from "node:module";
3
4
  import { basename, resolve } from "node:path";
4
5
  import { cwd, exit } from "node:process";
5
6
  import { fileURLToPath } from "node:url";
@@ -13,7 +14,8 @@ import { loadSetupOutput } from "@longtable/setup";
13
14
  import { answerWorkspaceQuestion, clearWorkspaceQuestion, createOrUpdateProjectWorkspace, createWorkspaceQuestion, inspectProjectWorkspace, loadProjectContextFromDirectory, loadWorkspaceState, syncCurrentWorkspaceView } from "@longtable/cli";
14
15
  import { buildFirstResearchShapeQuestion, firstResearchShapeAnswerConfirms, firstResearchShapeAnswerStatus } from "./first-research-shape.js";
15
16
  const SERVER_NAME = "longtable-state";
16
- const SERVER_VERSION = "0.1.39";
17
+ const require = createRequire(import.meta.url);
18
+ const SERVER_VERSION = String(require("../package.json").version ?? "0.0.0");
17
19
  const TOOL_NAMES = [
18
20
  "read_project",
19
21
  "read_session",
@@ -22,6 +24,7 @@ const TOOL_NAMES = [
22
24
  "begin_interview",
23
25
  "append_interview_turn",
24
26
  "summarize_interview",
27
+ "cancel_interview",
25
28
  "confirm_first_research_shape",
26
29
  "pending_questions",
27
30
  "evaluate_checkpoint",
@@ -169,9 +172,10 @@ function resolveFirstResearchShapeObligation(state, options) {
169
172
  };
170
173
  }
171
174
  function interviewDepth(turns) {
172
- const usableTurns = turns.filter((turn) => turn.quality !== "thin").length;
173
- if (usableTurns >= 3)
175
+ if (turns.some((turn) => turn.readyToSummarize === true && turn.quality !== "thin")) {
174
176
  return "ready_to_summarize";
177
+ }
178
+ const usableTurns = turns.filter((turn) => turn.quality !== "thin").length;
175
179
  if (usableTurns >= 1)
176
180
  return "forming_first_handle";
177
181
  return "gathering_context";
@@ -250,6 +254,10 @@ async function appendInterviewTurn(context, options) {
250
254
  const followUpQuestion = needsFollowUp
251
255
  ? options.followUpQuestion ?? defaultFollowUpQuestion(options.answer)
252
256
  : options.followUpQuestion;
257
+ const readyToSummarize = options.readyToSummarize === true && quality !== "thin";
258
+ const readinessRationale = options.readinessRationale
259
+ ?.map((rationale) => rationale.trim())
260
+ .filter(Boolean);
253
261
  const timestamp = new Date().toISOString();
254
262
  const turn = {
255
263
  id: createId("interview_turn"),
@@ -261,6 +269,8 @@ async function appendInterviewTurn(context, options) {
261
269
  quality,
262
270
  needsFollowUp,
263
271
  ...(followUpQuestion?.trim() ? { followUpQuestion: followUpQuestion.trim() } : {}),
272
+ ...(readyToSummarize ? { readyToSummarize } : {}),
273
+ ...(readinessRationale && readinessRationale.length > 0 ? { readinessRationale } : {}),
264
274
  ...(options.rationale && options.rationale.length > 0 ? { rationale: options.rationale } : {})
265
275
  };
266
276
  const turns = [...existing.turns, turn];
@@ -273,7 +283,10 @@ async function appendInterviewTurn(context, options) {
273
283
  turns,
274
284
  qualityNotes: [
275
285
  ...existing.qualityNotes,
276
- ...(needsFollowUp ? [`Turn ${turn.index} needs follow-up: ${followUpQuestion}`] : [])
286
+ ...(needsFollowUp ? [`Turn ${turn.index} needs follow-up: ${followUpQuestion}`] : []),
287
+ ...(readyToSummarize
288
+ ? [`Turn ${turn.index} marked ready to summarize: ${(readinessRationale ?? ["content-based readiness signal"]).join("; ")}`]
289
+ : [])
277
290
  ]
278
291
  };
279
292
  const updated = upsertInterviewHook(state, hook);
@@ -347,6 +360,53 @@ async function summarizeInterviewHook(context, options) {
347
360
  await syncCurrentWorkspaceView(context);
348
361
  return { hook, shape, state: updated, session };
349
362
  }
363
+ async function cancelInterviewHook(context, options) {
364
+ const state = asInterviewState(await loadWorkspaceState(context));
365
+ const existing = activeInterviewHook(state, options.hookId);
366
+ if (!existing) {
367
+ throw new Error("No active LongTable interview hook was found to cancel.");
368
+ }
369
+ const timestamp = new Date().toISOString();
370
+ const hook = {
371
+ ...existing,
372
+ status: "cancelled",
373
+ updatedAt: timestamp,
374
+ rationale: [
375
+ ...existing.rationale,
376
+ options.reason?.trim()
377
+ ? `Interview cancelled explicitly: ${options.reason.trim()}`
378
+ : "Interview cancelled explicitly by the researcher."
379
+ ]
380
+ };
381
+ const updated = upsertInterviewHook(state, hook);
382
+ const workingState = {
383
+ ...(updated.workingState ?? {})
384
+ };
385
+ if (workingState.activeInterviewHookId === existing.id) {
386
+ delete workingState.activeInterviewHookId;
387
+ delete workingState.interviewSurface;
388
+ delete workingState.interviewOpeningQuestion;
389
+ delete workingState.interviewSeedAnswer;
390
+ }
391
+ updated.workingState = workingState;
392
+ updated.narrativeTraces = [
393
+ ...(updated.narrativeTraces ?? []),
394
+ {
395
+ id: createId("narrative_trace"),
396
+ timestamp,
397
+ source: "$longtable-interview",
398
+ traceType: "judgment",
399
+ summary: options.reason?.trim()
400
+ ? `LongTable interview cancelled: ${options.reason.trim()}.`
401
+ : "LongTable interview cancelled before First Research Shape confirmation.",
402
+ visibility: "explicit",
403
+ importance: "low"
404
+ }
405
+ ];
406
+ await writeFile(context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
407
+ await syncCurrentWorkspaceView(context);
408
+ return { hook, state: updated };
409
+ }
350
410
  function findQuestion(records, questionId) {
351
411
  if (questionId) {
352
412
  return records.find((record) => record.id === questionId) ?? null;
@@ -709,9 +769,11 @@ export function createLongTableMcpServer() {
709
769
  quality: z.enum(["thin", "usable", "rich"]).optional(),
710
770
  needsFollowUp: z.boolean().optional(),
711
771
  followUpQuestion: z.string().optional(),
772
+ readyToSummarize: z.boolean().optional().describe("Set true only when the interview has content-based closure readiness; never infer this from turn count alone."),
773
+ readinessRationale: z.array(z.string()).optional().describe("Brief evidence that the interview has enough context for a First Research Shape."),
712
774
  rationale: z.array(z.string()).optional()
713
775
  })
714
- }, async ({ cwd: inputCwd, hookId, question, answer, reflection, quality, needsFollowUp, followUpQuestion, rationale }) => {
776
+ }, async ({ cwd: inputCwd, hookId, question, answer, reflection, quality, needsFollowUp, followUpQuestion, readyToSummarize, readinessRationale, rationale }) => {
715
777
  try {
716
778
  const context = await requireContext(inputCwd);
717
779
  const result = await appendInterviewTurn(context, {
@@ -722,6 +784,8 @@ export function createLongTableMcpServer() {
722
784
  quality: quality,
723
785
  needsFollowUp,
724
786
  followUpQuestion,
787
+ readyToSummarize,
788
+ readinessRationale,
725
789
  rationale
726
790
  });
727
791
  return textResult(result);
@@ -759,6 +823,26 @@ export function createLongTableMcpServer() {
759
823
  return errorResult(error instanceof Error ? error.message : String(error));
760
824
  }
761
825
  });
826
+ server.registerTool("cancel_interview", {
827
+ title: "Cancel LongTable Interview",
828
+ description: "Explicitly cancel the active $longtable-interview hook without confirming a First Research Shape.",
829
+ inputSchema: cwdSchema.extend({
830
+ hookId: z.string().optional(),
831
+ reason: z.string().optional()
832
+ })
833
+ }, async ({ cwd: inputCwd, hookId, reason }) => {
834
+ try {
835
+ const context = await requireContext(inputCwd);
836
+ const result = await cancelInterviewHook(context, { hookId, reason });
837
+ return textResult({
838
+ hook: compactInterviewHook(result.hook),
839
+ cancelled: true
840
+ });
841
+ }
842
+ catch (error) {
843
+ return errorResult(error instanceof Error ? error.message : String(error));
844
+ }
845
+ });
762
846
  server.registerTool("confirm_first_research_shape", {
763
847
  title: "Confirm First Research Shape",
764
848
  description: "Use MCP form elicitation to confirm, revise, defer, or request more context for the First Research Shape.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/mcp",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
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.40",
30
- "@longtable/cli": "0.1.40",
31
- "@longtable/core": "0.1.40",
32
- "@longtable/provider-claude": "0.1.40",
33
- "@longtable/provider-codex": "0.1.40",
34
- "@longtable/setup": "0.1.40",
29
+ "@longtable/checkpoints": "0.1.42",
30
+ "@longtable/cli": "0.1.42",
31
+ "@longtable/core": "0.1.42",
32
+ "@longtable/provider-claude": "0.1.42",
33
+ "@longtable/provider-codex": "0.1.42",
34
+ "@longtable/setup": "0.1.42",
35
35
  "@modelcontextprotocol/sdk": "^1.29.0",
36
36
  "zod": "^4.0.0"
37
37
  },