@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 +2 -1
- package/dist/first-research-shape.js +59 -17
- package/dist/server.js +89 -5
- package/package.json +7 -7
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.
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
shape.
|
|
89
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
30
|
-
"@longtable/cli": "0.1.
|
|
31
|
-
"@longtable/core": "0.1.
|
|
32
|
-
"@longtable/provider-claude": "0.1.
|
|
33
|
-
"@longtable/provider-codex": "0.1.
|
|
34
|
-
"@longtable/setup": "0.1.
|
|
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
|
},
|