@longtable/mcp 0.1.43 → 0.1.44

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/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./first-research-shape.js";
2
+ export * from "./research-specification.js";
2
3
  export * from "./server.js";
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./first-research-shape.js";
2
+ export * from "./research-specification.js";
2
3
  export * from "./server.js";
@@ -0,0 +1,65 @@
1
+ import type { QuestionOption } from "@longtable/core";
2
+ export interface ResearchSpecification {
3
+ title: string;
4
+ status?: "draft" | "confirmed" | "deferred";
5
+ createdAt?: string;
6
+ updatedAt?: string;
7
+ sourceHookId?: string;
8
+ researchDirection: {
9
+ question?: string;
10
+ purpose: string;
11
+ scopeBoundary?: string;
12
+ inclusionCriteria?: string[];
13
+ exclusionCriteria?: string[];
14
+ };
15
+ constructOntology: {
16
+ coreConstructs: string[];
17
+ distinctions: string[];
18
+ termsToAvoidCollapsing?: string[];
19
+ };
20
+ theoryAndFraming: {
21
+ anchors: string[];
22
+ alternatives?: string[];
23
+ overreachRisks?: string[];
24
+ };
25
+ measurementCoding: {
26
+ variablesOrConstructs: string[];
27
+ evidenceTypes: string[];
28
+ codingRules: string[];
29
+ openStandards?: string[];
30
+ };
31
+ methodAnalysis: {
32
+ design?: string;
33
+ analysisOptions: string[];
34
+ dataSufficiencyCriteria?: string[];
35
+ unsettledChoices?: string[];
36
+ };
37
+ evidenceAccess: {
38
+ requiredSources?: string[];
39
+ accessRequirements?: string[];
40
+ evidenceStandards?: string[];
41
+ };
42
+ epistemicAlignment: {
43
+ researcherKnowledge?: string[];
44
+ projectStatePriority?: string[];
45
+ aiInferenceLimits?: string[];
46
+ conflictResolutionRule?: string;
47
+ };
48
+ protectedDecisions: string[];
49
+ openQuestions: string[];
50
+ nextActions: string[];
51
+ confidence: "low" | "medium" | "high";
52
+ confirmedAt?: string;
53
+ }
54
+ export interface ResearchSpecificationQuestionSpec {
55
+ prompt: string;
56
+ title: string;
57
+ question: string;
58
+ checkpointKey: string;
59
+ options: QuestionOption[];
60
+ displayReason: string;
61
+ }
62
+ export declare function renderResearchSpecificationPreview(specification: ResearchSpecification): string;
63
+ export declare function buildResearchSpecificationQuestion(specification: ResearchSpecification): ResearchSpecificationQuestionSpec;
64
+ export declare function researchSpecificationAnswerConfirms(answer: string): boolean;
65
+ export declare function researchSpecificationAnswerStatus(answer: string): "confirmed" | "active" | "deferred";
@@ -0,0 +1,108 @@
1
+ function compactList(values, limit = 3) {
2
+ const kept = (values ?? []).map((value) => value.trim()).filter(Boolean).slice(0, limit);
3
+ return kept.length > 0 ? kept.join("; ") : "not specified";
4
+ }
5
+ function usesKorean(specification) {
6
+ const values = [
7
+ specification.title,
8
+ specification.researchDirection.question,
9
+ specification.researchDirection.purpose,
10
+ specification.researchDirection.scopeBoundary,
11
+ ...specification.constructOntology.coreConstructs,
12
+ ...specification.constructOntology.distinctions,
13
+ ...specification.theoryAndFraming.anchors,
14
+ ...specification.measurementCoding.codingRules,
15
+ ...specification.methodAnalysis.analysisOptions,
16
+ specification.epistemicAlignment.conflictResolutionRule,
17
+ ...specification.protectedDecisions,
18
+ ...specification.openQuestions,
19
+ ...specification.nextActions
20
+ ];
21
+ return values.some((value) => typeof value === "string" && /[ㄱ-ㅎㅏ-ㅣ가-힣]/.test(value));
22
+ }
23
+ export function renderResearchSpecificationPreview(specification) {
24
+ const korean = usesKorean(specification);
25
+ const lines = [
26
+ korean ? "Research Specification Preview" : "Research Specification Preview",
27
+ `${korean ? "제목" : "Title"}: ${specification.title}`,
28
+ `${korean ? "목적" : "Purpose"}: ${specification.researchDirection.purpose}`,
29
+ specification.researchDirection.scopeBoundary
30
+ ? `${korean ? "범위" : "Scope"}: ${specification.researchDirection.scopeBoundary}`
31
+ : undefined,
32
+ `${korean ? "핵심 construct" : "Core constructs"}: ${compactList(specification.constructOntology.coreConstructs)}`,
33
+ `${korean ? "구분" : "Distinctions"}: ${compactList(specification.constructOntology.distinctions)}`,
34
+ `${korean ? "이론 앵커" : "Theory anchors"}: ${compactList(specification.theoryAndFraming.anchors)}`,
35
+ `${korean ? "코딩 규칙" : "Coding rules"}: ${compactList(specification.measurementCoding.codingRules)}`,
36
+ `${korean ? "분석 옵션" : "Analysis options"}: ${compactList(specification.methodAnalysis.analysisOptions)}`,
37
+ specification.epistemicAlignment.conflictResolutionRule
38
+ ? `${korean ? "충돌 조정" : "Conflict rule"}: ${specification.epistemicAlignment.conflictResolutionRule}`
39
+ : undefined,
40
+ `${korean ? "열린 질문" : "Open questions"}: ${compactList(specification.openQuestions, 2)}`,
41
+ `${korean ? "다음 행동" : "Next actions"}: ${compactList(specification.nextActions, 2)}`,
42
+ `${korean ? "신뢰도" : "Confidence"}: ${specification.confidence}`
43
+ ].filter(Boolean);
44
+ return lines.join("\n");
45
+ }
46
+ function baseOptions(specification) {
47
+ const korean = usesKorean(specification);
48
+ return [
49
+ {
50
+ value: "confirm_specification",
51
+ label: korean ? "저장/확정" : "Confirm and save",
52
+ description: korean
53
+ ? "이 Research Specification을 현재 연구 명세로 저장하고 이어갑니다."
54
+ : "Save this Research Specification as the current research specification.",
55
+ recommended: specification.confidence !== "low"
56
+ },
57
+ {
58
+ value: "ask_one_more",
59
+ label: korean ? "한 질문 더" : "Ask one more question",
60
+ description: korean
61
+ ? "저장하기 전에 가장 위험한 빈칸 하나를 더 묻습니다."
62
+ : "Ask one more high-risk clarification before saving it.",
63
+ recommended: specification.confidence === "low"
64
+ },
65
+ {
66
+ value: "revise_section",
67
+ label: korean ? "섹션 수정" : "Revise a section",
68
+ description: korean
69
+ ? "범위, construct, 이론, 코딩, 방법, 접근, 정렬 중 특정 섹션을 수정합니다."
70
+ : "Revise one section such as scope, constructs, theory, coding, method, access, or alignment."
71
+ },
72
+ {
73
+ value: "keep_open",
74
+ label: korean ? "열어두기" : "Keep open",
75
+ description: korean
76
+ ? "아직 이 명세를 확정하지 않고 draft로 둡니다."
77
+ : "Keep this specification as a draft instead of confirming it."
78
+ }
79
+ ];
80
+ }
81
+ export function buildResearchSpecificationQuestion(specification) {
82
+ const korean = usesKorean(specification);
83
+ const preview = renderResearchSpecificationPreview(specification);
84
+ return {
85
+ prompt: preview,
86
+ title: korean ? "Research Specification 확인" : "Research Specification Confirmation",
87
+ question: korean
88
+ ? "이 Research Specification을 어떻게 처리할까요?"
89
+ : "How should LongTable handle this Research Specification?",
90
+ checkpointKey: "research_specification_confirmation",
91
+ options: baseOptions(specification),
92
+ displayReason: korean
93
+ ? `${preview}\n\n인터뷰가 단순한 방향 요약을 넘어 연구 명세를 만들 만큼 구체화되었습니다. 저장 전에 범위, construct, 이론, 코딩, 방법, 접근, 정렬을 명시적으로 확인해야 합니다.`
94
+ : `${preview}\n\nThe interview has moved beyond a short direction summary into a research specification. Scope, constructs, theory, coding, method, access, and epistemic alignment should be explicit before saving.`
95
+ };
96
+ }
97
+ export function researchSpecificationAnswerConfirms(answer) {
98
+ return answer === "confirm_specification";
99
+ }
100
+ export function researchSpecificationAnswerStatus(answer) {
101
+ if (researchSpecificationAnswerConfirms(answer)) {
102
+ return "confirmed";
103
+ }
104
+ if (answer === "keep_open") {
105
+ return "deferred";
106
+ }
107
+ return "active";
108
+ }
package/dist/server.js CHANGED
@@ -13,6 +13,7 @@ import { renderQuestionRecordPrompt } from "@longtable/provider-codex";
13
13
  import { loadSetupOutput } from "@longtable/setup";
14
14
  import { answerWorkspaceQuestion, clearWorkspaceQuestion, createOrUpdateProjectWorkspace, createWorkspaceQuestion, inspectProjectWorkspace, loadProjectContextFromDirectory, loadWorkspaceState, syncCurrentWorkspaceView } from "@longtable/cli";
15
15
  import { buildFirstResearchShapeQuestion, firstResearchShapeAnswerConfirms, firstResearchShapeAnswerStatus } from "./first-research-shape.js";
16
+ import { buildResearchSpecificationQuestion, renderResearchSpecificationPreview, researchSpecificationAnswerConfirms, researchSpecificationAnswerStatus } from "./research-specification.js";
16
17
  const SERVER_NAME = "longtable-state";
17
18
  const require = createRequire(import.meta.url);
18
19
  const SERVER_VERSION = String(require("../package.json").version ?? "0.0.0");
@@ -24,8 +25,11 @@ const TOOL_NAMES = [
24
25
  "begin_interview",
25
26
  "append_interview_turn",
26
27
  "summarize_interview",
28
+ "summarize_research_specification",
29
+ "read_research_specification",
27
30
  "cancel_interview",
28
31
  "confirm_first_research_shape",
32
+ "confirm_research_specification",
29
33
  "pending_questions",
30
34
  "evaluate_checkpoint",
31
35
  "create_question",
@@ -56,6 +60,59 @@ const firstResearchShapeSchema = z.object({
56
60
  sourceHookId: z.string().optional(),
57
61
  confirmedAt: z.string().optional()
58
62
  });
63
+ const optionalStringArraySchema = z.array(z.string().min(1)).optional();
64
+ const researchSpecificationSchema = z.object({
65
+ title: z.string().min(1),
66
+ status: z.enum(["draft", "confirmed", "deferred"]).optional(),
67
+ createdAt: z.string().optional(),
68
+ updatedAt: z.string().optional(),
69
+ sourceHookId: z.string().optional(),
70
+ researchDirection: z.object({
71
+ question: z.string().optional(),
72
+ purpose: z.string().min(1),
73
+ scopeBoundary: z.string().optional(),
74
+ inclusionCriteria: optionalStringArraySchema,
75
+ exclusionCriteria: optionalStringArraySchema
76
+ }),
77
+ constructOntology: z.object({
78
+ coreConstructs: z.array(z.string().min(1)).default([]),
79
+ distinctions: z.array(z.string().min(1)).default([]),
80
+ termsToAvoidCollapsing: optionalStringArraySchema
81
+ }),
82
+ theoryAndFraming: z.object({
83
+ anchors: z.array(z.string().min(1)).default([]),
84
+ alternatives: optionalStringArraySchema,
85
+ overreachRisks: optionalStringArraySchema
86
+ }),
87
+ measurementCoding: z.object({
88
+ variablesOrConstructs: z.array(z.string().min(1)).default([]),
89
+ evidenceTypes: z.array(z.string().min(1)).default([]),
90
+ codingRules: z.array(z.string().min(1)).default([]),
91
+ openStandards: optionalStringArraySchema
92
+ }),
93
+ methodAnalysis: z.object({
94
+ design: z.string().optional(),
95
+ analysisOptions: z.array(z.string().min(1)).default([]),
96
+ dataSufficiencyCriteria: optionalStringArraySchema,
97
+ unsettledChoices: optionalStringArraySchema
98
+ }),
99
+ evidenceAccess: z.object({
100
+ requiredSources: optionalStringArraySchema,
101
+ accessRequirements: optionalStringArraySchema,
102
+ evidenceStandards: optionalStringArraySchema
103
+ }),
104
+ epistemicAlignment: z.object({
105
+ researcherKnowledge: optionalStringArraySchema,
106
+ projectStatePriority: optionalStringArraySchema,
107
+ aiInferenceLimits: optionalStringArraySchema,
108
+ conflictResolutionRule: z.string().optional()
109
+ }),
110
+ protectedDecisions: z.array(z.string().min(1)).default([]),
111
+ openQuestions: z.array(z.string().min(1)).default([]),
112
+ nextActions: z.array(z.string().min(1)).default([]),
113
+ confidence: z.enum(["low", "medium", "high"]).default("medium"),
114
+ confirmedAt: z.string().optional()
115
+ });
59
116
  function textResult(structuredContent) {
60
117
  return {
61
118
  content: [
@@ -360,6 +417,119 @@ async function summarizeInterviewHook(context, options) {
360
417
  await syncCurrentWorkspaceView(context);
361
418
  return { hook, shape, state: updated, session };
362
419
  }
420
+ function normalizeStringArray(values) {
421
+ return (values ?? []).map((value) => value.trim()).filter(Boolean);
422
+ }
423
+ function normalizeOptionalString(value) {
424
+ const trimmed = value?.trim();
425
+ return trimmed && trimmed.length > 0 ? trimmed : undefined;
426
+ }
427
+ function normalizeResearchSpecification(input, sourceHookId, timestamp) {
428
+ const title = input.title.trim();
429
+ const purpose = input.researchDirection.purpose.trim();
430
+ if (!title || !purpose) {
431
+ throw new Error("Research Specification title and researchDirection.purpose are required.");
432
+ }
433
+ return {
434
+ title,
435
+ status: input.confirmedAt ? "confirmed" : input.status ?? "draft",
436
+ createdAt: input.createdAt ?? timestamp,
437
+ updatedAt: timestamp,
438
+ ...(sourceHookId ? { sourceHookId } : {}),
439
+ researchDirection: {
440
+ ...(normalizeOptionalString(input.researchDirection.question) ? { question: normalizeOptionalString(input.researchDirection.question) } : {}),
441
+ purpose,
442
+ ...(normalizeOptionalString(input.researchDirection.scopeBoundary) ? { scopeBoundary: normalizeOptionalString(input.researchDirection.scopeBoundary) } : {}),
443
+ inclusionCriteria: normalizeStringArray(input.researchDirection.inclusionCriteria),
444
+ exclusionCriteria: normalizeStringArray(input.researchDirection.exclusionCriteria)
445
+ },
446
+ constructOntology: {
447
+ coreConstructs: normalizeStringArray(input.constructOntology.coreConstructs),
448
+ distinctions: normalizeStringArray(input.constructOntology.distinctions),
449
+ termsToAvoidCollapsing: normalizeStringArray(input.constructOntology.termsToAvoidCollapsing)
450
+ },
451
+ theoryAndFraming: {
452
+ anchors: normalizeStringArray(input.theoryAndFraming.anchors),
453
+ alternatives: normalizeStringArray(input.theoryAndFraming.alternatives),
454
+ overreachRisks: normalizeStringArray(input.theoryAndFraming.overreachRisks)
455
+ },
456
+ measurementCoding: {
457
+ variablesOrConstructs: normalizeStringArray(input.measurementCoding.variablesOrConstructs),
458
+ evidenceTypes: normalizeStringArray(input.measurementCoding.evidenceTypes),
459
+ codingRules: normalizeStringArray(input.measurementCoding.codingRules),
460
+ openStandards: normalizeStringArray(input.measurementCoding.openStandards)
461
+ },
462
+ methodAnalysis: {
463
+ ...(normalizeOptionalString(input.methodAnalysis.design) ? { design: normalizeOptionalString(input.methodAnalysis.design) } : {}),
464
+ analysisOptions: normalizeStringArray(input.methodAnalysis.analysisOptions),
465
+ dataSufficiencyCriteria: normalizeStringArray(input.methodAnalysis.dataSufficiencyCriteria),
466
+ unsettledChoices: normalizeStringArray(input.methodAnalysis.unsettledChoices)
467
+ },
468
+ evidenceAccess: {
469
+ requiredSources: normalizeStringArray(input.evidenceAccess.requiredSources),
470
+ accessRequirements: normalizeStringArray(input.evidenceAccess.accessRequirements),
471
+ evidenceStandards: normalizeStringArray(input.evidenceAccess.evidenceStandards)
472
+ },
473
+ epistemicAlignment: {
474
+ researcherKnowledge: normalizeStringArray(input.epistemicAlignment.researcherKnowledge),
475
+ projectStatePriority: normalizeStringArray(input.epistemicAlignment.projectStatePriority),
476
+ aiInferenceLimits: normalizeStringArray(input.epistemicAlignment.aiInferenceLimits),
477
+ ...(normalizeOptionalString(input.epistemicAlignment.conflictResolutionRule)
478
+ ? { conflictResolutionRule: normalizeOptionalString(input.epistemicAlignment.conflictResolutionRule) }
479
+ : {})
480
+ },
481
+ protectedDecisions: normalizeStringArray(input.protectedDecisions),
482
+ openQuestions: normalizeStringArray(input.openQuestions),
483
+ nextActions: normalizeStringArray(input.nextActions),
484
+ confidence: input.confidence,
485
+ ...(input.confirmedAt ? { confirmedAt: input.confirmedAt } : {})
486
+ };
487
+ }
488
+ async function summarizeResearchSpecificationHook(context, options) {
489
+ const state = asInterviewState(await loadWorkspaceState(context));
490
+ const sourceHookId = options.hookId
491
+ ?? options.specification.sourceHookId
492
+ ?? state.firstResearchShape?.sourceHookId;
493
+ const existing = sourceHookId
494
+ ? (state.hooks ?? []).find((hook) => isInterviewHookRun(hook) && hook.id === sourceHookId)
495
+ : activeInterviewHook(state);
496
+ const timestamp = new Date().toISOString();
497
+ const specification = normalizeResearchSpecification(options.specification, existing?.id ?? sourceHookId, timestamp);
498
+ const hook = existing
499
+ ? {
500
+ ...existing,
501
+ status: "ready_to_confirm",
502
+ updatedAt: timestamp,
503
+ researchSpecification: specification
504
+ }
505
+ : undefined;
506
+ const session = {
507
+ ...context.session,
508
+ lastUpdatedAt: timestamp,
509
+ researchSpecification: specification,
510
+ resumeHint: `I want to continue from the Research Specification: ${specification.title}.`
511
+ };
512
+ context.session = session;
513
+ const updated = hook ? upsertInterviewHook(state, hook) : state;
514
+ updated.researchSpecification = specification;
515
+ updated.workingState = {
516
+ ...updated.workingState,
517
+ researchSpecification: specification
518
+ };
519
+ updated.narrativeTraces.push({
520
+ id: createId("narrative_trace"),
521
+ timestamp,
522
+ source: "$longtable-interview",
523
+ traceType: "judgment",
524
+ summary: `Research Specification draft: ${specification.title}.`,
525
+ visibility: "explicit",
526
+ importance: specification.confidence
527
+ });
528
+ await writeFile(context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
529
+ await writeFile(context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
530
+ await syncCurrentWorkspaceView(context);
531
+ return { hook, specification, state: updated, session };
532
+ }
363
533
  async function cancelInterviewHook(context, options) {
364
534
  const state = asInterviewState(await loadWorkspaceState(context));
365
535
  const existing = activeInterviewHook(state, options.hookId);
@@ -430,6 +600,7 @@ function compactInterviewHook(hook) {
430
600
  provider: hook.provider,
431
601
  turnCount: hook.turns?.length ?? 0,
432
602
  firstResearchShape: hook.firstResearchShape?.handle,
603
+ researchSpecification: hook.researchSpecification?.title,
433
604
  createdAt: hook.createdAt,
434
605
  updatedAt: hook.updatedAt
435
606
  };
@@ -591,6 +762,87 @@ async function markAlreadyConfirmedFirstResearchShape(context, shape) {
591
762
  await syncCurrentWorkspaceView(context);
592
763
  return { state: nextState, session, shape: confirmedShape };
593
764
  }
765
+ async function markResearchSpecificationConfirmation(context, specification, answer, questionId, decisionId) {
766
+ const state = asInterviewState(await loadWorkspaceState(context));
767
+ const timestamp = new Date().toISOString();
768
+ const confirmedSpecification = researchSpecificationAnswerConfirms(answer)
769
+ ? { ...specification, status: "confirmed", confirmedAt: timestamp, updatedAt: timestamp }
770
+ : {
771
+ ...specification,
772
+ status: researchSpecificationAnswerStatus(answer) === "deferred" ? "deferred" : "draft",
773
+ updatedAt: timestamp
774
+ };
775
+ state.researchSpecification = confirmedSpecification;
776
+ state.workingState = {
777
+ ...state.workingState,
778
+ researchSpecification: confirmedSpecification
779
+ };
780
+ state.hooks = (state.hooks ?? []).map((hook) => {
781
+ if (hook.id !== specification.sourceHookId || !isInterviewHookRun(hook)) {
782
+ return hook;
783
+ }
784
+ return {
785
+ ...hook,
786
+ status: researchSpecificationAnswerStatus(answer),
787
+ updatedAt: timestamp,
788
+ researchSpecification: confirmedSpecification,
789
+ linkedQuestionRecordIds: questionId
790
+ ? [...(hook.linkedQuestionRecordIds ?? []), questionId]
791
+ : hook.linkedQuestionRecordIds,
792
+ linkedDecisionRecordIds: decisionId
793
+ ? [...(hook.linkedDecisionRecordIds ?? []), decisionId]
794
+ : hook.linkedDecisionRecordIds
795
+ };
796
+ });
797
+ const session = {
798
+ ...context.session,
799
+ researchSpecification: confirmedSpecification,
800
+ lastUpdatedAt: timestamp
801
+ };
802
+ context.session = session;
803
+ await writeFile(context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
804
+ await writeFile(context.stateFilePath, JSON.stringify(state, null, 2), "utf8");
805
+ await syncCurrentWorkspaceView(context);
806
+ return { state, session, specification: confirmedSpecification };
807
+ }
808
+ async function markAlreadyConfirmedResearchSpecification(context, specification) {
809
+ const state = asInterviewState(await loadWorkspaceState(context));
810
+ const timestamp = new Date().toISOString();
811
+ const confirmedSpecification = specification.confirmedAt
812
+ ? specification
813
+ : { ...specification, status: "confirmed", confirmedAt: timestamp, updatedAt: timestamp };
814
+ state.researchSpecification = confirmedSpecification;
815
+ state.workingState = {
816
+ ...state.workingState,
817
+ researchSpecification: confirmedSpecification
818
+ };
819
+ state.hooks = (state.hooks ?? []).map((hook) => {
820
+ if (!isInterviewHookRun(hook)) {
821
+ return hook;
822
+ }
823
+ const matchesSource = specification.sourceHookId && hook.id === specification.sourceHookId;
824
+ const matchesTitle = !specification.sourceHookId && hook.researchSpecification?.title === specification.title;
825
+ if (!matchesSource && !matchesTitle) {
826
+ return hook;
827
+ }
828
+ return {
829
+ ...hook,
830
+ status: "confirmed",
831
+ updatedAt: timestamp,
832
+ researchSpecification: confirmedSpecification
833
+ };
834
+ });
835
+ const session = {
836
+ ...context.session,
837
+ researchSpecification: confirmedSpecification,
838
+ lastUpdatedAt: timestamp
839
+ };
840
+ context.session = session;
841
+ await writeFile(context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
842
+ await writeFile(context.stateFilePath, JSON.stringify(state, null, 2), "utf8");
843
+ await syncCurrentWorkspaceView(context);
844
+ return { state, session, specification: confirmedSpecification };
845
+ }
594
846
  function statusForElicitationError(error) {
595
847
  const message = error instanceof Error ? error.message : String(error);
596
848
  if (/timed?\s*out|timeout/i.test(message)) {
@@ -823,6 +1075,63 @@ export function createLongTableMcpServer() {
823
1075
  return errorResult(error instanceof Error ? error.message : String(error));
824
1076
  }
825
1077
  });
1078
+ server.registerTool("summarize_research_specification", {
1079
+ title: "Summarize LongTable Research Specification",
1080
+ description: "Store the fuller Research Specification after a First Research Shape has enough interview context to preserve scope, constructs, theory, coding, method, evidence/access, and epistemic alignment.",
1081
+ inputSchema: cwdSchema.extend({
1082
+ hookId: z.string().optional(),
1083
+ specification: researchSpecificationSchema
1084
+ })
1085
+ }, async ({ cwd: inputCwd, hookId, specification }) => {
1086
+ try {
1087
+ const context = await requireContext(inputCwd);
1088
+ const result = await summarizeResearchSpecificationHook(context, {
1089
+ hookId,
1090
+ specification: specification
1091
+ });
1092
+ return textResult({
1093
+ hook: compactInterviewHook(result.hook),
1094
+ specification: result.specification,
1095
+ preview: renderResearchSpecificationPreview(result.specification),
1096
+ session: {
1097
+ currentGoal: result.session.currentGoal,
1098
+ currentBlocker: result.session.currentBlocker,
1099
+ nextAction: result.session.nextAction,
1100
+ firstResearchShape: result.session.firstResearchShape,
1101
+ researchSpecification: result.session.researchSpecification
1102
+ }
1103
+ });
1104
+ }
1105
+ catch (error) {
1106
+ return errorResult(error instanceof Error ? error.message : String(error));
1107
+ }
1108
+ });
1109
+ server.registerTool("read_research_specification", {
1110
+ title: "Read LongTable Research Specification",
1111
+ description: "Read the current Research Specification and render the researcher-facing preview.",
1112
+ inputSchema: cwdSchema
1113
+ }, async ({ cwd: inputCwd }) => {
1114
+ try {
1115
+ const context = await requireContext(inputCwd);
1116
+ const state = asInterviewState(await loadWorkspaceState(context));
1117
+ const session = context.session;
1118
+ const specification = state.researchSpecification ?? session.researchSpecification;
1119
+ if (!specification) {
1120
+ return textResult({
1121
+ found: false,
1122
+ message: "No Research Specification was found. Run summarize_research_specification first."
1123
+ });
1124
+ }
1125
+ return textResult({
1126
+ found: true,
1127
+ specification,
1128
+ preview: renderResearchSpecificationPreview(specification)
1129
+ });
1130
+ }
1131
+ catch (error) {
1132
+ return errorResult(error instanceof Error ? error.message : String(error));
1133
+ }
1134
+ });
826
1135
  server.registerTool("cancel_interview", {
827
1136
  title: "Cancel LongTable Interview",
828
1137
  description: "Explicitly cancel the active $longtable-interview hook without confirming a First Research Shape.",
@@ -954,6 +1263,118 @@ export function createLongTableMcpServer() {
954
1263
  return errorResult(error instanceof Error ? error.message : String(error));
955
1264
  }
956
1265
  });
1266
+ server.registerTool("confirm_research_specification", {
1267
+ title: "Confirm Research Specification",
1268
+ description: "Use MCP form elicitation to confirm, revise, defer, or request one more question for the full Research Specification.",
1269
+ inputSchema: cwdSchema.extend({
1270
+ specification: researchSpecificationSchema.optional(),
1271
+ provider: z.enum(["codex", "claude"]).default("codex"),
1272
+ fallbackOnly: z.boolean().default(false)
1273
+ })
1274
+ }, async ({ cwd: inputCwd, specification: inputSpecification, provider, fallbackOnly }) => {
1275
+ try {
1276
+ const context = await requireContext(inputCwd);
1277
+ const state = asInterviewState(await loadWorkspaceState(context));
1278
+ const session = context.session;
1279
+ const specification = inputSpecification
1280
+ ?? state.researchSpecification
1281
+ ?? session.researchSpecification;
1282
+ if (!specification) {
1283
+ return errorResult("No Research Specification was found to confirm. Run summarize_research_specification first.");
1284
+ }
1285
+ if (specification.confirmedAt) {
1286
+ const confirmation = await markAlreadyConfirmedResearchSpecification(context, specification);
1287
+ return textResult({
1288
+ specification: confirmation.specification,
1289
+ preview: renderResearchSpecificationPreview(confirmation.specification),
1290
+ elicitation: { attempted: false, reason: "already_confirmed" }
1291
+ });
1292
+ }
1293
+ const spec = buildResearchSpecificationQuestion(specification);
1294
+ const created = await createWorkspaceQuestion({
1295
+ context,
1296
+ prompt: spec.prompt,
1297
+ title: spec.title,
1298
+ question: spec.question,
1299
+ checkpointKey: spec.checkpointKey,
1300
+ questionOptions: spec.options,
1301
+ displayReason: spec.displayReason,
1302
+ provider: provider,
1303
+ required: true
1304
+ });
1305
+ const fallback = renderQuestionFallback(created.question, provider);
1306
+ if (fallbackOnly) {
1307
+ const marked = await markQuestionTransport(context, created.question.id, "fallback_rendered", "MCP elicitation skipped by fallbackOnly.");
1308
+ return textResult({
1309
+ question: marked ?? created.question,
1310
+ specification,
1311
+ preview: renderResearchSpecificationPreview(specification),
1312
+ elicitation: { attempted: false, reason: "fallbackOnly" },
1313
+ fallback
1314
+ });
1315
+ }
1316
+ try {
1317
+ await markQuestionTransport(context, created.question.id, "attempted");
1318
+ const elicited = await server.server.elicitInput(buildElicitationParams(created.question));
1319
+ const accepted = acceptedAnswer(elicited);
1320
+ if (!accepted) {
1321
+ const status = elicited.action === "decline" || elicited.action === "cancel"
1322
+ ? "declined"
1323
+ : "fallback_rendered";
1324
+ const marked = await markQuestionTransport(context, created.question.id, status, `MCP elicitation returned action: ${elicited.action}.`);
1325
+ const cleared = status === "declined"
1326
+ ? await clearWorkspaceQuestion({
1327
+ context,
1328
+ questionId: created.question.id,
1329
+ reason: `MCP elicitation returned action: ${elicited.action}; Research Specification confirmation was deferred.`
1330
+ })
1331
+ : undefined;
1332
+ return textResult({
1333
+ question: cleared?.question ?? marked ?? created.question,
1334
+ specification,
1335
+ preview: renderResearchSpecificationPreview(specification),
1336
+ elicitation: { attempted: true, action: elicited.action },
1337
+ fallback
1338
+ });
1339
+ }
1340
+ const decided = await answerWorkspaceQuestion({
1341
+ context,
1342
+ questionId: created.question.id,
1343
+ answer: accepted.answer,
1344
+ provider: provider,
1345
+ surface: "mcp_elicitation"
1346
+ });
1347
+ const marked = await markQuestionTransport(context, created.question.id, "accepted");
1348
+ const confirmation = await markResearchSpecificationConfirmation(context, specification, accepted.answer, created.question.id, decided.decision.id);
1349
+ return textResult({
1350
+ specification: confirmation.specification,
1351
+ preview: renderResearchSpecificationPreview(confirmation.specification),
1352
+ question: marked ? { ...decided.question, transportStatus: marked.transportStatus } : decided.question,
1353
+ decision: decided.decision,
1354
+ elicitation: { attempted: true, action: elicited.action }
1355
+ });
1356
+ }
1357
+ catch (elicitationError) {
1358
+ const status = statusForElicitationError(elicitationError);
1359
+ const message = elicitationError instanceof Error ? elicitationError.message : String(elicitationError);
1360
+ const marked = await markQuestionTransport(context, created.question.id, status, message);
1361
+ return textResult({
1362
+ question: marked ?? created.question,
1363
+ specification,
1364
+ preview: renderResearchSpecificationPreview(specification),
1365
+ elicitation: {
1366
+ attempted: true,
1367
+ supported: status !== "unsupported" ? undefined : false,
1368
+ error: message
1369
+ },
1370
+ fallback
1371
+ });
1372
+ }
1373
+ }
1374
+ catch (error) {
1375
+ return errorResult(error instanceof Error ? error.message : String(error));
1376
+ }
1377
+ });
957
1378
  server.registerTool("pending_questions", {
958
1379
  title: "List Pending Researcher Checkpoints",
959
1380
  description: "List pending LongTable QuestionRecords.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/mcp",
3
- "version": "0.1.43",
3
+ "version": "0.1.44",
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.43",
30
- "@longtable/cli": "0.1.43",
31
- "@longtable/core": "0.1.43",
32
- "@longtable/provider-claude": "0.1.43",
33
- "@longtable/provider-codex": "0.1.43",
34
- "@longtable/setup": "0.1.43",
29
+ "@longtable/checkpoints": "0.1.44",
30
+ "@longtable/cli": "0.1.44",
31
+ "@longtable/core": "0.1.44",
32
+ "@longtable/provider-claude": "0.1.44",
33
+ "@longtable/provider-codex": "0.1.44",
34
+ "@longtable/setup": "0.1.44",
35
35
  "@modelcontextprotocol/sdk": "^1.29.0",
36
36
  "zod": "^4.0.0"
37
37
  },