@longtable/mcp 0.1.32 → 0.1.33

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.32
18
+ npx -y @longtable/mcp@0.1.33
19
19
  ```
20
20
 
21
21
  Self-test:
@@ -0,0 +1,25 @@
1
+ import type { QuestionOption } from "@longtable/core";
2
+ export interface FirstResearchShape {
3
+ handle: string;
4
+ currentGoal: string;
5
+ currentBlocker?: string;
6
+ researchObject?: string;
7
+ gapRisk?: string;
8
+ protectedDecision?: string;
9
+ openQuestions: string[];
10
+ nextAction: string;
11
+ confidence: "low" | "medium" | "high";
12
+ sourceHookId?: string;
13
+ confirmedAt?: string;
14
+ }
15
+ export interface FirstResearchShapeQuestionSpec {
16
+ prompt: string;
17
+ title: string;
18
+ question: string;
19
+ checkpointKey: string;
20
+ options: QuestionOption[];
21
+ displayReason: string;
22
+ }
23
+ export declare function buildFirstResearchShapeQuestion(shape: FirstResearchShape): FirstResearchShapeQuestionSpec;
24
+ export declare function firstResearchShapeAnswerConfirms(answer: string): boolean;
25
+ export declare function firstResearchShapeAnswerStatus(answer: string): "confirmed" | "active" | "deferred";
@@ -0,0 +1,111 @@
1
+ function questionTitle(shape) {
2
+ if (shape.protectedDecision) {
3
+ return "Protected Research Decision";
4
+ }
5
+ if (shape.currentBlocker) {
6
+ return "Main Unresolved Issue";
7
+ }
8
+ return "Emerging Research Direction";
9
+ }
10
+ function questionText(shape) {
11
+ if (shape.protectedDecision) {
12
+ return `Before LongTable moves forward, what should stay explicitly open about ${shape.protectedDecision}?`;
13
+ }
14
+ if (shape.currentBlocker) {
15
+ return "What should LongTable do with the main unresolved issue in this emerging study?";
16
+ }
17
+ if (shape.openQuestions.length > 0) {
18
+ return "What should LongTable keep explicit as the next research move?";
19
+ }
20
+ return "How should LongTable carry this emerging study forward?";
21
+ }
22
+ function displayReason(shape) {
23
+ if (shape.protectedDecision) {
24
+ return `The interview surfaced a protected decision: ${shape.protectedDecision}. LongTable should not let it settle silently.`;
25
+ }
26
+ if (shape.currentBlocker) {
27
+ return `The main blocker is still open: ${shape.currentBlocker}. The next move should make that uncertainty explicit.`;
28
+ }
29
+ if (shape.openQuestions.length > 0) {
30
+ return `The interview surfaced open research questions. Choose the next move that keeps the most important one visible.`;
31
+ }
32
+ return "LongTable has enough context to propose a provisional research direction, but the next research move should still be explicit.";
33
+ }
34
+ function recommendedValue(shape) {
35
+ if (shape.protectedDecision) {
36
+ return "protect_decision";
37
+ }
38
+ if (shape.confidence === "high" && !shape.currentBlocker) {
39
+ return "stabilize_shape";
40
+ }
41
+ if (shape.currentBlocker || shape.confidence === "low") {
42
+ return "gather_context";
43
+ }
44
+ return "stabilize_shape";
45
+ }
46
+ function baseOptions(shape) {
47
+ const recommended = recommendedValue(shape);
48
+ const options = [];
49
+ if (shape.protectedDecision) {
50
+ options.push({
51
+ value: "protect_decision",
52
+ label: `Keep ${shape.protectedDecision} open`,
53
+ description: "Treat this as the guarded judgment while the broader direction stays provisional.",
54
+ recommended: recommended === "protect_decision"
55
+ });
56
+ }
57
+ options.push({
58
+ value: "stabilize_shape",
59
+ label: "Use this as the provisional direction",
60
+ description: "Carry this research shape forward as the current working direction.",
61
+ recommended: recommended === "stabilize_shape"
62
+ }, {
63
+ value: "gather_context",
64
+ label: "Gather one more concrete case",
65
+ description: "Ask for one more scene, source, dataset, or example before stabilizing it.",
66
+ recommended: recommended === "gather_context"
67
+ }, {
68
+ value: "revise_shape",
69
+ label: "Revise the emerging shape",
70
+ description: "Change the handle, blocker, or next action before LongTable treats it as usable."
71
+ });
72
+ if (!shape.protectedDecision) {
73
+ options.push({
74
+ value: "keep_open",
75
+ label: "Keep the study open longer",
76
+ description: "Do not stabilize this direction yet."
77
+ });
78
+ }
79
+ return options;
80
+ }
81
+ export function buildFirstResearchShapeQuestion(shape) {
82
+ 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}`
90
+ ].filter(Boolean);
91
+ return {
92
+ prompt: summaryLines.join("\n"),
93
+ title: questionTitle(shape),
94
+ question: questionText(shape),
95
+ checkpointKey: "first_research_shape_confirmation",
96
+ options: baseOptions(shape),
97
+ displayReason: displayReason(shape)
98
+ };
99
+ }
100
+ export function firstResearchShapeAnswerConfirms(answer) {
101
+ return answer === "stabilize_shape" || answer === "protect_decision";
102
+ }
103
+ export function firstResearchShapeAnswerStatus(answer) {
104
+ if (firstResearchShapeAnswerConfirms(answer)) {
105
+ return "confirmed";
106
+ }
107
+ if (answer === "keep_open") {
108
+ return "deferred";
109
+ }
110
+ return "active";
111
+ }
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
+ export * from "./first-research-shape.js";
1
2
  export * from "./server.js";
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
+ export * from "./first-research-shape.js";
1
2
  export * from "./server.js";
package/dist/server.js CHANGED
@@ -11,8 +11,9 @@ import { renderQuestionRecordInput } from "@longtable/provider-claude";
11
11
  import { renderQuestionRecordPrompt } from "@longtable/provider-codex";
12
12
  import { loadSetupOutput } from "@longtable/setup";
13
13
  import { answerWorkspaceQuestion, createOrUpdateProjectWorkspace, createWorkspaceQuestion, inspectProjectWorkspace, loadProjectContextFromDirectory, loadWorkspaceState, syncCurrentWorkspaceView } from "@longtable/cli";
14
+ import { buildFirstResearchShapeQuestion, firstResearchShapeAnswerConfirms, firstResearchShapeAnswerStatus } from "./first-research-shape.js";
14
15
  const SERVER_NAME = "longtable-state";
15
- const SERVER_VERSION = "0.1.32";
16
+ const SERVER_VERSION = "0.1.33";
16
17
  const TOOL_NAMES = [
17
18
  "read_project",
18
19
  "read_session",
@@ -48,7 +49,9 @@ const firstResearchShapeSchema = z.object({
48
49
  protectedDecision: z.string().optional(),
49
50
  openQuestions: z.array(z.string().min(1)).default([]),
50
51
  nextAction: z.string().min(1),
51
- confidence: z.enum(["low", "medium", "high"]).default("medium")
52
+ confidence: z.enum(["low", "medium", "high"]).default("medium"),
53
+ sourceHookId: z.string().optional(),
54
+ confirmedAt: z.string().optional()
52
55
  });
53
56
  function textResult(structuredContent) {
54
57
  return {
@@ -88,10 +91,14 @@ function createId(prefix) {
88
91
  function asInterviewState(state) {
89
92
  return state;
90
93
  }
94
+ function isInterviewHookRun(hook) {
95
+ return hook?.kind === "longtable_interview";
96
+ }
91
97
  function activeInterviewHook(state, hookId) {
92
98
  const hooks = state.hooks ?? [];
93
99
  if (hookId) {
94
- return hooks.find((hook) => hook.id === hookId);
100
+ const hook = hooks.find((candidate) => candidate.id === hookId);
101
+ return isInterviewHookRun(hook) ? hook : undefined;
95
102
  }
96
103
  return [...hooks].reverse().find((hook) => hook.kind === "longtable_interview" &&
97
104
  (hook.status === "pending" || hook.status === "active" || hook.status === "ready_to_confirm"));
@@ -106,6 +113,61 @@ function upsertInterviewHook(state, hook) {
106
113
  hooks: nextHooks
107
114
  };
108
115
  }
116
+ function upsertQuestionObligation(state, obligation) {
117
+ const current = state.questionObligations ?? [];
118
+ const next = current.some((entry) => entry.id === obligation.id)
119
+ ? current.map((entry) => entry.id === obligation.id ? obligation : entry)
120
+ : [...current, obligation];
121
+ return {
122
+ ...state,
123
+ questionObligations: next
124
+ };
125
+ }
126
+ function ensureFirstResearchShapeObligation(state, shape, options) {
127
+ const existing = (state.questionObligations ?? []).find((obligation) => obligation.kind === "first_research_shape_confirmation" &&
128
+ obligation.status === "pending" &&
129
+ obligation.sourceHookId === shape.sourceHookId);
130
+ if (!existing) {
131
+ return upsertQuestionObligation(state, {
132
+ id: createId("question_obligation"),
133
+ kind: "first_research_shape_confirmation",
134
+ status: "pending",
135
+ createdAt: new Date().toISOString(),
136
+ updatedAt: new Date().toISOString(),
137
+ prompt: options.prompt,
138
+ reason: options.reason,
139
+ ...(shape.sourceHookId ? { sourceHookId: shape.sourceHookId } : {}),
140
+ ...(options.questionId ? { questionId: options.questionId } : {})
141
+ });
142
+ }
143
+ return upsertQuestionObligation(state, {
144
+ ...existing,
145
+ updatedAt: new Date().toISOString(),
146
+ prompt: options.prompt,
147
+ reason: options.reason,
148
+ ...(options.questionId ? { questionId: options.questionId } : existing.questionId ? { questionId: existing.questionId } : {}),
149
+ ...(shape.sourceHookId ? { sourceHookId: shape.sourceHookId } : {})
150
+ });
151
+ }
152
+ function resolveFirstResearchShapeObligation(state, options) {
153
+ return {
154
+ ...state,
155
+ questionObligations: (state.questionObligations ?? []).map((obligation) => {
156
+ const matches = obligation.kind === "first_research_shape_confirmation" && ((options.sourceHookId && obligation.sourceHookId === options.sourceHookId) ||
157
+ (options.questionId && obligation.questionId === options.questionId));
158
+ if (!matches || obligation.status !== "pending") {
159
+ return obligation;
160
+ }
161
+ return {
162
+ ...obligation,
163
+ status: options.status ?? "satisfied",
164
+ updatedAt: new Date().toISOString(),
165
+ ...(options.questionId ? { questionId: options.questionId } : obligation.questionId ? { questionId: obligation.questionId } : {}),
166
+ ...(options.decisionId ? { decisionId: options.decisionId } : obligation.decisionId ? { decisionId: obligation.decisionId } : {})
167
+ };
168
+ })
169
+ };
170
+ }
109
171
  function interviewDepth(turns) {
110
172
  const usableTurns = turns.filter((turn) => turn.quality !== "thin").length;
111
173
  if (usableTurns >= 3)
@@ -267,10 +329,15 @@ async function summarizeInterviewHook(context, options) {
267
329
  visibility: "explicit",
268
330
  importance: shape.confidence
269
331
  });
332
+ const questionSpec = buildFirstResearchShapeQuestion(shape);
333
+ const withObligation = ensureFirstResearchShapeObligation(updated, shape, {
334
+ prompt: questionSpec.question,
335
+ reason: questionSpec.displayReason
336
+ });
270
337
  await writeFile(context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
271
- await writeFile(context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
338
+ await writeFile(context.stateFilePath, JSON.stringify(withObligation, null, 2), "utf8");
272
339
  await syncCurrentWorkspaceView(context);
273
- return { hook, shape, state: updated, session };
340
+ return { hook, shape, state: withObligation, session };
274
341
  }
275
342
  function findQuestion(records, questionId) {
276
343
  if (questionId) {
@@ -355,31 +422,10 @@ function acceptedAnswer(result) {
355
422
  answer
356
423
  };
357
424
  }
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
425
  async function markFirstResearchShapeConfirmation(context, shape, answer, questionId, decisionId) {
380
426
  const state = asInterviewState(await loadWorkspaceState(context));
381
427
  const timestamp = new Date().toISOString();
382
- const confirmedShape = answer === "confirm"
428
+ const confirmedShape = firstResearchShapeAnswerConfirms(answer)
383
429
  ? { ...shape, confirmedAt: timestamp }
384
430
  : shape;
385
431
  state.firstResearchShape = confirmedShape;
@@ -388,12 +434,12 @@ async function markFirstResearchShapeConfirmation(context, shape, answer, questi
388
434
  firstResearchShape: confirmedShape
389
435
  };
390
436
  state.hooks = (state.hooks ?? []).map((hook) => {
391
- if (hook.id !== shape.sourceHookId) {
437
+ if (hook.id !== shape.sourceHookId || !isInterviewHookRun(hook)) {
392
438
  return hook;
393
439
  }
394
440
  return {
395
441
  ...hook,
396
- status: answer === "confirm" ? "confirmed" : answer === "defer" ? "deferred" : "active",
442
+ status: firstResearchShapeAnswerStatus(answer),
397
443
  updatedAt: timestamp,
398
444
  firstResearchShape: confirmedShape,
399
445
  linkedQuestionRecordIds: questionId
@@ -404,6 +450,12 @@ async function markFirstResearchShapeConfirmation(context, shape, answer, questi
404
450
  : hook.linkedDecisionRecordIds
405
451
  };
406
452
  });
453
+ const nextState = resolveFirstResearchShapeObligation(state, {
454
+ sourceHookId: shape.sourceHookId,
455
+ questionId,
456
+ decisionId,
457
+ status: "satisfied"
458
+ });
407
459
  const session = {
408
460
  ...context.session,
409
461
  firstResearchShape: confirmedShape,
@@ -411,9 +463,9 @@ async function markFirstResearchShapeConfirmation(context, shape, answer, questi
411
463
  };
412
464
  context.session = session;
413
465
  await writeFile(context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
414
- await writeFile(context.stateFilePath, JSON.stringify(state, null, 2), "utf8");
466
+ await writeFile(context.stateFilePath, JSON.stringify(nextState, null, 2), "utf8");
415
467
  await syncCurrentWorkspaceView(context);
416
- return { state, session, shape: confirmedShape };
468
+ return { state: nextState, session, shape: confirmedShape };
417
469
  }
418
470
  function statusForElicitationError(error) {
419
471
  const message = error instanceof Error ? error.message : String(error);
@@ -651,6 +703,13 @@ export function createLongTableMcpServer() {
651
703
  provider: provider,
652
704
  required: true
653
705
  });
706
+ const createdState = ensureFirstResearchShapeObligation(asInterviewState(await loadWorkspaceState(context)), shape, {
707
+ prompt: spec.question,
708
+ reason: spec.displayReason,
709
+ questionId: created.question.id
710
+ });
711
+ await writeFile(context.stateFilePath, JSON.stringify(createdState, null, 2), "utf8");
712
+ await syncCurrentWorkspaceView(context);
654
713
  const fallback = renderQuestionFallback(created.question, provider);
655
714
  if (fallbackOnly) {
656
715
  const marked = await markQuestionTransport(context, created.question.id, "fallback_rendered", "MCP elicitation skipped by fallbackOnly.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/mcp",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
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.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",
29
+ "@longtable/checkpoints": "0.1.33",
30
+ "@longtable/cli": "0.1.33",
31
+ "@longtable/core": "0.1.33",
32
+ "@longtable/provider-claude": "0.1.33",
33
+ "@longtable/provider-codex": "0.1.33",
34
+ "@longtable/setup": "0.1.33",
35
35
  "@modelcontextprotocol/sdk": "^1.29.0",
36
36
  "zod": "^4.0.0"
37
37
  },