@longtable/mcp 0.1.11
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 +24 -0
- package/bin/longtable-state +7 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +309 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# @longtable/mcp
|
|
2
|
+
|
|
3
|
+
MCP transport for LongTable workspace state and Researcher Checkpoints.
|
|
4
|
+
|
|
5
|
+
This package does not own LongTable state. It exposes structured tools over the
|
|
6
|
+
existing `.longtable/` source of truth.
|
|
7
|
+
|
|
8
|
+
Server name:
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
longtable-state
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Run:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx -y @longtable/mcp@0.1.11
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Self-test:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
longtable-state --self-test
|
|
24
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./server.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./server.js";
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { cwd, exit } from "node:process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { classifyCheckpointTrigger } from "@longtable/checkpoints";
|
|
10
|
+
import { renderQuestionRecordInput } from "@longtable/provider-claude";
|
|
11
|
+
import { renderQuestionRecordPrompt } from "@longtable/provider-codex";
|
|
12
|
+
import { answerWorkspaceQuestion, createWorkspaceQuestion, inspectProjectWorkspace, loadProjectContextFromDirectory, loadWorkspaceState, syncCurrentWorkspaceView } from "@longtable/cli";
|
|
13
|
+
const SERVER_NAME = "longtable-state";
|
|
14
|
+
const SERVER_VERSION = "0.1.11";
|
|
15
|
+
const TOOL_NAMES = [
|
|
16
|
+
"read_project",
|
|
17
|
+
"read_session",
|
|
18
|
+
"inspect_workspace",
|
|
19
|
+
"pending_questions",
|
|
20
|
+
"evaluate_checkpoint",
|
|
21
|
+
"create_question",
|
|
22
|
+
"render_question",
|
|
23
|
+
"append_decision",
|
|
24
|
+
"regenerate_current"
|
|
25
|
+
];
|
|
26
|
+
const cwdSchema = z.object({
|
|
27
|
+
cwd: z.string().optional().describe("LongTable project directory or child path. Defaults to server cwd.")
|
|
28
|
+
});
|
|
29
|
+
function textResult(structuredContent) {
|
|
30
|
+
return {
|
|
31
|
+
content: [
|
|
32
|
+
{
|
|
33
|
+
type: "text",
|
|
34
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
structuredContent
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function errorResult(message) {
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: "text",
|
|
45
|
+
text: message
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
isError: true
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function resolveStartPath(input) {
|
|
52
|
+
return resolve(input ?? cwd());
|
|
53
|
+
}
|
|
54
|
+
async function requireContext(startPath) {
|
|
55
|
+
const context = await loadProjectContextFromDirectory(resolveStartPath(startPath));
|
|
56
|
+
if (!context) {
|
|
57
|
+
throw new Error("No LongTable workspace was found from the supplied cwd.");
|
|
58
|
+
}
|
|
59
|
+
return context;
|
|
60
|
+
}
|
|
61
|
+
function findQuestion(records, questionId) {
|
|
62
|
+
if (questionId) {
|
|
63
|
+
return records.find((record) => record.id === questionId) ?? null;
|
|
64
|
+
}
|
|
65
|
+
return records.filter((record) => record.status === "pending").at(-1) ?? null;
|
|
66
|
+
}
|
|
67
|
+
async function readAllowedProjectFiles(context) {
|
|
68
|
+
const current = existsSync(context.currentFilePath)
|
|
69
|
+
? await readFile(context.currentFilePath, "utf8")
|
|
70
|
+
: "";
|
|
71
|
+
const agentsPath = resolve(context.project.projectPath, "AGENTS.md");
|
|
72
|
+
const agents = existsSync(agentsPath)
|
|
73
|
+
? await readFile(agentsPath, "utf8")
|
|
74
|
+
: "";
|
|
75
|
+
return {
|
|
76
|
+
current,
|
|
77
|
+
agentsPath,
|
|
78
|
+
agents
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export function createLongTableMcpServer() {
|
|
82
|
+
const server = new McpServer({
|
|
83
|
+
name: SERVER_NAME,
|
|
84
|
+
version: SERVER_VERSION
|
|
85
|
+
}, {
|
|
86
|
+
instructions: "Use LongTable state tools to inspect .longtable workspaces, evaluate Researcher Checkpoints, write QuestionRecords, append DecisionRecords, and regenerate CURRENT.md. Treat .longtable as the source of truth."
|
|
87
|
+
});
|
|
88
|
+
server.registerTool("read_project", {
|
|
89
|
+
title: "Read LongTable Project",
|
|
90
|
+
description: "Read project metadata from a LongTable workspace.",
|
|
91
|
+
inputSchema: cwdSchema,
|
|
92
|
+
annotations: { readOnlyHint: true }
|
|
93
|
+
}, async ({ cwd: inputCwd }) => {
|
|
94
|
+
try {
|
|
95
|
+
const context = await requireContext(inputCwd);
|
|
96
|
+
return textResult({
|
|
97
|
+
project: context.project,
|
|
98
|
+
files: {
|
|
99
|
+
project: context.projectFilePath,
|
|
100
|
+
session: context.sessionFilePath,
|
|
101
|
+
state: context.stateFilePath,
|
|
102
|
+
current: context.currentFilePath
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
return errorResult(error instanceof Error ? error.message : String(error));
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
server.registerTool("read_session", {
|
|
111
|
+
title: "Read LongTable Session",
|
|
112
|
+
description: "Read the current LongTable session record.",
|
|
113
|
+
inputSchema: cwdSchema,
|
|
114
|
+
annotations: { readOnlyHint: true }
|
|
115
|
+
}, async ({ cwd: inputCwd }) => {
|
|
116
|
+
try {
|
|
117
|
+
const context = await requireContext(inputCwd);
|
|
118
|
+
return textResult({ session: context.session });
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return errorResult(error instanceof Error ? error.message : String(error));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
server.registerTool("inspect_workspace", {
|
|
125
|
+
title: "Inspect LongTable Workspace",
|
|
126
|
+
description: "Inspect workspace files, counts, recent invocations, questions, and decisions.",
|
|
127
|
+
inputSchema: cwdSchema.extend({
|
|
128
|
+
includeFiles: z.boolean().default(false).describe("Include CURRENT.md and AGENTS.md text.")
|
|
129
|
+
}),
|
|
130
|
+
annotations: { readOnlyHint: true }
|
|
131
|
+
}, async ({ cwd: inputCwd, includeFiles }) => {
|
|
132
|
+
try {
|
|
133
|
+
const context = await loadProjectContextFromDirectory(resolveStartPath(inputCwd));
|
|
134
|
+
const inspection = await inspectProjectWorkspace(resolveStartPath(inputCwd));
|
|
135
|
+
if (!context || !includeFiles) {
|
|
136
|
+
return textResult({ inspection });
|
|
137
|
+
}
|
|
138
|
+
return textResult({
|
|
139
|
+
inspection,
|
|
140
|
+
files: await readAllowedProjectFiles(context)
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return errorResult(error instanceof Error ? error.message : String(error));
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
server.registerTool("pending_questions", {
|
|
148
|
+
title: "List Pending Researcher Checkpoints",
|
|
149
|
+
description: "List pending LongTable QuestionRecords.",
|
|
150
|
+
inputSchema: cwdSchema,
|
|
151
|
+
annotations: { readOnlyHint: true }
|
|
152
|
+
}, async ({ cwd: inputCwd }) => {
|
|
153
|
+
try {
|
|
154
|
+
const context = await requireContext(inputCwd);
|
|
155
|
+
const state = await loadWorkspaceState(context);
|
|
156
|
+
const pending = (state.questionLog ?? []).filter((record) => record.status === "pending");
|
|
157
|
+
return textResult({
|
|
158
|
+
pending,
|
|
159
|
+
required: pending.filter((record) => record.prompt.required)
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
return errorResult(error instanceof Error ? error.message : String(error));
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
server.registerTool("evaluate_checkpoint", {
|
|
167
|
+
title: "Evaluate Checkpoint Trigger",
|
|
168
|
+
description: "Classify natural-language context into a LongTable checkpoint signal without writing state.",
|
|
169
|
+
inputSchema: cwdSchema.extend({
|
|
170
|
+
prompt: z.string().min(1),
|
|
171
|
+
mode: z.enum(["explore", "review", "critique", "draft", "commit", "submit"]).optional()
|
|
172
|
+
}),
|
|
173
|
+
annotations: { readOnlyHint: true }
|
|
174
|
+
}, async ({ cwd: inputCwd, prompt, mode }) => {
|
|
175
|
+
try {
|
|
176
|
+
const context = await loadProjectContextFromDirectory(resolveStartPath(inputCwd));
|
|
177
|
+
const state = context ? await loadWorkspaceState(context) : undefined;
|
|
178
|
+
const classification = classifyCheckpointTrigger(prompt, {
|
|
179
|
+
preferredMode: mode,
|
|
180
|
+
unresolvedTensions: state?.openTensions ?? [],
|
|
181
|
+
studyContract: state?.studyContract
|
|
182
|
+
});
|
|
183
|
+
return textResult({ classification });
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
return errorResult(error instanceof Error ? error.message : String(error));
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
server.registerTool("create_question", {
|
|
190
|
+
title: "Create Researcher Checkpoint",
|
|
191
|
+
description: "Create a pending QuestionRecord in the LongTable workspace.",
|
|
192
|
+
inputSchema: cwdSchema.extend({
|
|
193
|
+
prompt: z.string().min(1),
|
|
194
|
+
title: z.string().optional(),
|
|
195
|
+
question: z.string().optional(),
|
|
196
|
+
provider: z.enum(["codex", "claude"]).optional(),
|
|
197
|
+
required: z.boolean().optional()
|
|
198
|
+
})
|
|
199
|
+
}, async ({ cwd: inputCwd, prompt, title, question, provider, required }) => {
|
|
200
|
+
try {
|
|
201
|
+
const context = await requireContext(inputCwd);
|
|
202
|
+
const result = await createWorkspaceQuestion({
|
|
203
|
+
context,
|
|
204
|
+
prompt,
|
|
205
|
+
title,
|
|
206
|
+
question,
|
|
207
|
+
provider,
|
|
208
|
+
required
|
|
209
|
+
});
|
|
210
|
+
return textResult({
|
|
211
|
+
question: result.question,
|
|
212
|
+
nextAction: `longtable decide --question ${result.question.id} --answer <value>`
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
return errorResult(error instanceof Error ? error.message : String(error));
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
server.registerTool("render_question", {
|
|
220
|
+
title: "Render Researcher Checkpoint",
|
|
221
|
+
description: "Render a pending QuestionRecord for Codex numbered prompt or Claude structured question transport.",
|
|
222
|
+
inputSchema: cwdSchema.extend({
|
|
223
|
+
questionId: z.string().optional(),
|
|
224
|
+
provider: z.enum(["codex", "claude"]).default("codex")
|
|
225
|
+
}),
|
|
226
|
+
annotations: { readOnlyHint: true }
|
|
227
|
+
}, async ({ cwd: inputCwd, questionId, provider }) => {
|
|
228
|
+
try {
|
|
229
|
+
const context = await requireContext(inputCwd);
|
|
230
|
+
const state = await loadWorkspaceState(context);
|
|
231
|
+
const question = findQuestion(state.questionLog ?? [], questionId);
|
|
232
|
+
if (!question) {
|
|
233
|
+
return errorResult("No matching pending LongTable question was found.");
|
|
234
|
+
}
|
|
235
|
+
const transport = provider === "claude"
|
|
236
|
+
? renderQuestionRecordInput(question)
|
|
237
|
+
: renderQuestionRecordPrompt(question);
|
|
238
|
+
return textResult({ provider, question, transport });
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
return errorResult(error instanceof Error ? error.message : String(error));
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
server.registerTool("append_decision", {
|
|
245
|
+
title: "Append LongTable Decision",
|
|
246
|
+
description: "Answer a pending QuestionRecord and append a DecisionRecord.",
|
|
247
|
+
inputSchema: cwdSchema.extend({
|
|
248
|
+
questionId: z.string().optional(),
|
|
249
|
+
answer: z.string().min(1),
|
|
250
|
+
rationale: z.string().optional(),
|
|
251
|
+
provider: z.enum(["codex", "claude"]).optional()
|
|
252
|
+
})
|
|
253
|
+
}, async ({ cwd: inputCwd, questionId, answer, rationale, provider }) => {
|
|
254
|
+
try {
|
|
255
|
+
const context = await requireContext(inputCwd);
|
|
256
|
+
const result = await answerWorkspaceQuestion({
|
|
257
|
+
context,
|
|
258
|
+
questionId,
|
|
259
|
+
answer,
|
|
260
|
+
rationale,
|
|
261
|
+
provider: provider
|
|
262
|
+
});
|
|
263
|
+
return textResult({
|
|
264
|
+
question: result.question,
|
|
265
|
+
decision: result.decision
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
return errorResult(error instanceof Error ? error.message : String(error));
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
server.registerTool("regenerate_current", {
|
|
273
|
+
title: "Regenerate CURRENT.md",
|
|
274
|
+
description: "Regenerate CURRENT.md from LongTable machine-readable state.",
|
|
275
|
+
inputSchema: cwdSchema
|
|
276
|
+
}, async ({ cwd: inputCwd }) => {
|
|
277
|
+
try {
|
|
278
|
+
const context = await requireContext(inputCwd);
|
|
279
|
+
const path = await syncCurrentWorkspaceView(context);
|
|
280
|
+
return textResult({ current: path });
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
return errorResult(error instanceof Error ? error.message : String(error));
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
return server;
|
|
287
|
+
}
|
|
288
|
+
export async function runStdioServer() {
|
|
289
|
+
const server = createLongTableMcpServer();
|
|
290
|
+
const transport = new StdioServerTransport();
|
|
291
|
+
await server.connect(transport);
|
|
292
|
+
console.error(`${SERVER_NAME} MCP server running on stdio`);
|
|
293
|
+
}
|
|
294
|
+
export async function runLongTableMcpCli(argv = process.argv) {
|
|
295
|
+
if (argv.includes("--self-test")) {
|
|
296
|
+
console.log(JSON.stringify({ name: SERVER_NAME, version: SERVER_VERSION, tools: TOOL_NAMES }, null, 2));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
await runStdioServer();
|
|
300
|
+
}
|
|
301
|
+
function isDirectRun() {
|
|
302
|
+
return process.argv[1] ? fileURLToPath(import.meta.url) === resolve(process.argv[1]) : false;
|
|
303
|
+
}
|
|
304
|
+
if (isDirectRun()) {
|
|
305
|
+
runLongTableMcpCli().catch((error) => {
|
|
306
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
307
|
+
exit(1);
|
|
308
|
+
});
|
|
309
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@longtable/mcp",
|
|
3
|
+
"version": "0.1.11",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "LongTable MCP transport for workspace state and Researcher Checkpoints",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"longtable-state": "./bin/longtable-state"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"bin",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -p tsconfig.json",
|
|
25
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
26
|
+
"self-test": "node ./dist/server.js --self-test"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@longtable/checkpoints": "0.1.11",
|
|
30
|
+
"@longtable/cli": "0.1.11",
|
|
31
|
+
"@longtable/core": "0.1.11",
|
|
32
|
+
"@longtable/provider-claude": "0.1.11",
|
|
33
|
+
"@longtable/provider-codex": "0.1.11",
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
35
|
+
"zod": "^4.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^22.10.1",
|
|
39
|
+
"typescript": "^5.6.0"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"longtable",
|
|
43
|
+
"mcp",
|
|
44
|
+
"research",
|
|
45
|
+
"checkpoints"
|
|
46
|
+
],
|
|
47
|
+
"author": "Hosung You",
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/HosungYou/LongTable.git"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/HosungYou/LongTable#readme",
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=18.0.0"
|
|
59
|
+
}
|
|
60
|
+
}
|