@longtable/mcp 0.1.30 → 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.
- package/README.md +14 -4
- package/dist/server.js +479 -4
- 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
|
|
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.
|
|
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
|
|
36
|
-
|
|
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 {
|
|
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.
|
|
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.
|
|
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.
|
|
30
|
-
"@longtable/cli": "0.1.
|
|
31
|
-
"@longtable/core": "0.1.
|
|
32
|
-
"@longtable/provider-claude": "0.1.
|
|
33
|
-
"@longtable/provider-codex": "0.1.
|
|
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
|
},
|