session-collab-mcp 2.1.0 → 2.3.0
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 +123 -54
- package/dist/{chunk-V4APNPXF.js → chunk-TFGXE3EI.js} +261 -17
- package/dist/chunk-TFGXE3EI.js.map +1 -0
- package/dist/cli.js +4 -11
- package/dist/cli.js.map +1 -1
- package/dist/http/cli.js +272 -84
- package/dist/http/cli.js.map +1 -1
- package/dist/http/client-cli.js +80 -6
- package/dist/http/client-cli.js.map +1 -1
- package/package.json +15 -8
- package/dist/chunk-V4APNPXF.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -6,25 +6,18 @@ import {
|
|
|
6
6
|
createLocalDatabase,
|
|
7
7
|
getDefaultDbPath,
|
|
8
8
|
getMcpTools,
|
|
9
|
-
handleMcpRequest
|
|
10
|
-
|
|
9
|
+
handleMcpRequest,
|
|
10
|
+
loadMigrationsFromDir
|
|
11
|
+
} from "./chunk-TFGXE3EI.js";
|
|
11
12
|
|
|
12
13
|
// src/cli.ts
|
|
13
14
|
import { createInterface } from "readline";
|
|
14
|
-
import { readFileSync } from "fs";
|
|
15
15
|
import { join, dirname } from "path";
|
|
16
16
|
import { fileURLToPath } from "url";
|
|
17
17
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
18
|
function loadMigrations() {
|
|
19
19
|
const migrationsDir = join(__dirname, "..", "migrations");
|
|
20
|
-
return
|
|
21
|
-
readFileSync(join(migrationsDir, "0001_init.sql"), "utf-8"),
|
|
22
|
-
readFileSync(join(migrationsDir, "0002_auth.sql"), "utf-8"),
|
|
23
|
-
readFileSync(join(migrationsDir, "0003_config.sql"), "utf-8"),
|
|
24
|
-
readFileSync(join(migrationsDir, "0004_symbols.sql"), "utf-8"),
|
|
25
|
-
readFileSync(join(migrationsDir, "0005_references.sql"), "utf-8"),
|
|
26
|
-
readFileSync(join(migrationsDir, "0006_composite_indexes.sql"), "utf-8")
|
|
27
|
-
];
|
|
20
|
+
return loadMigrationsFromDir(migrationsDir);
|
|
28
21
|
}
|
|
29
22
|
function parseArgs() {
|
|
30
23
|
const args = process.argv.slice(2);
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n// Local MCP server for session collaboration\n// Runs via stdio, stores data in ~/.claude/session-collab/collab.db\n\nimport { createInterface } from 'readline';\nimport { createLocalDatabase, getDefaultDbPath } from './db/sqlite-adapter.js';\nimport { handleMcpRequest, getMcpTools } from './mcp/server.js';\nimport { VERSION, SERVER_NAME, SERVER_INSTRUCTIONS } from './constants.js';\nimport {
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n// Local MCP server for session collaboration\n// Runs via stdio, stores data in ~/.claude/session-collab/collab.db\n\nimport { createInterface } from 'readline';\nimport { createLocalDatabase, getDefaultDbPath } from './db/sqlite-adapter.js';\nimport { loadMigrationsFromDir } from './db/migrations.js';\nimport { handleMcpRequest, getMcpTools } from './mcp/server.js';\nimport { VERSION, SERVER_NAME, SERVER_INSTRUCTIONS } from './constants.js';\nimport { join, dirname } from 'path';\nimport { fileURLToPath } from 'url';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n// Load migrations in filename order. Historical duplicate numeric prefixes are\n// preserved, so the full filename defines the execution order.\nfunction loadMigrations(): string[] {\n const migrationsDir = join(__dirname, '..', 'migrations');\n return loadMigrationsFromDir(migrationsDir);\n}\n\n// Parse command line arguments\nfunction parseArgs(): { dbPath?: string } {\n const args = process.argv.slice(2);\n let dbPath: string | undefined;\n\n for (let i = 0; i < args.length; i++) {\n if (args[i] === '--db' && args[i + 1]) {\n dbPath = args[i + 1];\n i++;\n }\n }\n\n return { dbPath };\n}\n\n// JSON-RPC response helpers\nfunction jsonRpcResponse(id: number | string | null, result: unknown): string {\n return JSON.stringify({ jsonrpc: '2.0', id, result });\n}\n\nfunction jsonRpcError(id: number | string | null, code: number, message: string): string {\n return JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } });\n}\n\nasync function main(): Promise<void> {\n const { dbPath } = parseArgs();\n const db = createLocalDatabase(dbPath);\n\n // Initialize schema\n try {\n const migrations = loadMigrations();\n db.initSchema(migrations);\n } catch (error) {\n // Schema might already exist, that's fine\n if (!(error instanceof Error && error.message.includes('already exists'))) {\n console.error('Warning: Migration error:', error);\n }\n }\n\n // Log startup to stderr (stdout is for JSON-RPC)\n console.error(`Session Collab MCP Server (local)`);\n console.error(`Database: ${dbPath ?? getDefaultDbPath()}`);\n\n const rl = createInterface({\n input: process.stdin,\n output: process.stdout,\n terminal: false,\n });\n\n rl.on('line', async (line) => {\n if (!line.trim()) return;\n\n try {\n const request = JSON.parse(line);\n const { id, method, params } = request;\n\n // Handle MCP protocol methods\n switch (method) {\n case 'initialize': {\n const response = jsonRpcResponse(id, {\n protocolVersion: '2024-11-05',\n capabilities: {\n tools: {},\n },\n serverInfo: {\n name: SERVER_NAME,\n version: VERSION,\n },\n instructions: SERVER_INSTRUCTIONS,\n });\n console.log(response);\n break;\n }\n\n case 'notifications/initialized': {\n // No response needed for notifications\n break;\n }\n\n case 'tools/list': {\n const tools = getMcpTools();\n const response = jsonRpcResponse(id, { tools });\n console.log(response);\n break;\n }\n\n case 'tools/call': {\n const result = await handleMcpRequest(\n db,\n params.name,\n params.arguments ?? {}\n );\n const response = jsonRpcResponse(id, result);\n console.log(response);\n break;\n }\n\n default: {\n const errorResponse = jsonRpcError(id, -32601, `Method not found: ${method}`);\n console.log(errorResponse);\n }\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Unknown error';\n const errorResponse = jsonRpcError(null, -32700, `Parse error: ${errorMessage}`);\n console.log(errorResponse);\n }\n });\n\n rl.on('close', () => {\n db.close();\n process.exit(0);\n });\n}\n\nmain().catch((error) => {\n console.error('Fatal error:', error);\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
|
package/dist/http/cli.js
CHANGED
|
@@ -1,62 +1,161 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
JsonRpcRequestSchema,
|
|
4
|
+
McpServer,
|
|
3
5
|
createLocalDatabase,
|
|
6
|
+
generateId,
|
|
4
7
|
getDefaultDbPath,
|
|
5
8
|
getMcpTools,
|
|
6
|
-
handleMcpRequest
|
|
7
|
-
|
|
9
|
+
handleMcpRequest,
|
|
10
|
+
loadMigrationsFromDir
|
|
11
|
+
} from "../chunk-TFGXE3EI.js";
|
|
8
12
|
|
|
9
13
|
// src/http/cli.ts
|
|
10
|
-
import { readFileSync } from "fs";
|
|
11
14
|
import { join, dirname } from "path";
|
|
12
15
|
import { fileURLToPath } from "url";
|
|
13
16
|
|
|
14
17
|
// src/http/server.ts
|
|
15
18
|
import http from "http";
|
|
16
19
|
import { URL } from "url";
|
|
17
|
-
|
|
20
|
+
var HttpRequestError = class extends Error {
|
|
21
|
+
constructor(status, code, message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.status = status;
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.name = "HttpRequestError";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
function normalizeHost(host) {
|
|
29
|
+
return host.trim().toLowerCase();
|
|
30
|
+
}
|
|
31
|
+
function normalizeHostHeader(rawHost) {
|
|
32
|
+
if (!rawHost) return null;
|
|
33
|
+
const first = rawHost.split(",")[0]?.trim();
|
|
34
|
+
if (!first) return null;
|
|
35
|
+
if (first.startsWith("[")) {
|
|
36
|
+
const closingIndex = first.indexOf("]");
|
|
37
|
+
if (closingIndex !== -1) {
|
|
38
|
+
return normalizeHost(first.slice(1, closingIndex));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const withoutPort = first.includes(":") ? first.split(":")[0] : first;
|
|
42
|
+
return normalizeHost(withoutPort);
|
|
43
|
+
}
|
|
44
|
+
function isLocalBindHost(host) {
|
|
45
|
+
const normalized = normalizeHost(host);
|
|
46
|
+
return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1";
|
|
47
|
+
}
|
|
48
|
+
function buildAllowedHosts(host, configuredHosts = []) {
|
|
49
|
+
const allowedHosts = new Set(configuredHosts.map(normalizeHost).filter(Boolean));
|
|
50
|
+
if (isLocalBindHost(host)) {
|
|
51
|
+
allowedHosts.add("localhost");
|
|
52
|
+
allowedHosts.add("127.0.0.1");
|
|
53
|
+
allowedHosts.add("::1");
|
|
54
|
+
} else if (host !== "0.0.0.0" && host !== "::") {
|
|
55
|
+
allowedHosts.add(normalizeHost(host));
|
|
56
|
+
}
|
|
57
|
+
return allowedHosts;
|
|
58
|
+
}
|
|
59
|
+
function getTraceId(req) {
|
|
60
|
+
const requestId = req.headers["x-request-id"];
|
|
61
|
+
if (typeof requestId === "string" && requestId.trim()) {
|
|
62
|
+
return requestId.trim();
|
|
63
|
+
}
|
|
64
|
+
return generateId();
|
|
65
|
+
}
|
|
66
|
+
function sendJson(res, status, body, traceId, contentType = "application/json") {
|
|
18
67
|
const payload = JSON.stringify(body);
|
|
19
68
|
res.writeHead(status, {
|
|
20
|
-
"Content-Type":
|
|
21
|
-
"Content-Length": Buffer.byteLength(payload)
|
|
69
|
+
"Content-Type": contentType,
|
|
70
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
71
|
+
"X-Request-ID": traceId
|
|
22
72
|
});
|
|
23
73
|
res.end(payload);
|
|
24
74
|
}
|
|
25
|
-
function
|
|
26
|
-
|
|
75
|
+
function sendHttpError(res, status, code, message, traceId) {
|
|
76
|
+
sendJson(res, status, { ok: false, error: code, code, message, trace_id: traceId }, traceId);
|
|
77
|
+
}
|
|
78
|
+
function sendJsonRpcResponse(res, status, response, traceId) {
|
|
79
|
+
sendJson(res, status, response, traceId);
|
|
80
|
+
}
|
|
81
|
+
function sendJsonRpcError(res, status, id, code, message, traceId) {
|
|
82
|
+
sendJson(
|
|
83
|
+
res,
|
|
84
|
+
status,
|
|
85
|
+
{
|
|
86
|
+
jsonrpc: "2.0",
|
|
87
|
+
id,
|
|
88
|
+
error: {
|
|
89
|
+
code,
|
|
90
|
+
message,
|
|
91
|
+
data: { trace_id: traceId }
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
traceId
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
async function readJsonBody(req) {
|
|
98
|
+
return await new Promise((resolve, reject) => {
|
|
27
99
|
let data = "";
|
|
100
|
+
let settled = false;
|
|
101
|
+
const rejectOnce = (error) => {
|
|
102
|
+
if (settled) return;
|
|
103
|
+
settled = true;
|
|
104
|
+
reject(error);
|
|
105
|
+
};
|
|
106
|
+
const resolveOnce = (value) => {
|
|
107
|
+
if (settled) return;
|
|
108
|
+
settled = true;
|
|
109
|
+
resolve(value);
|
|
110
|
+
};
|
|
28
111
|
req.on("data", (chunk) => {
|
|
112
|
+
if (settled) return;
|
|
29
113
|
data += chunk;
|
|
30
114
|
if (data.length > 1e6) {
|
|
31
|
-
|
|
115
|
+
rejectOnce(new HttpRequestError(413, "PAYLOAD_TOO_LARGE", "Payload too large"));
|
|
116
|
+
req.pause();
|
|
32
117
|
}
|
|
33
118
|
});
|
|
34
119
|
req.on("end", () => {
|
|
120
|
+
if (settled) return;
|
|
35
121
|
if (!data) {
|
|
36
|
-
|
|
122
|
+
resolveOnce(void 0);
|
|
37
123
|
return;
|
|
38
124
|
}
|
|
39
125
|
try {
|
|
40
|
-
|
|
41
|
-
} catch
|
|
42
|
-
|
|
126
|
+
resolveOnce(JSON.parse(data));
|
|
127
|
+
} catch {
|
|
128
|
+
rejectOnce(new HttpRequestError(400, "INVALID_JSON", "Request body must be valid JSON"));
|
|
43
129
|
}
|
|
44
130
|
});
|
|
131
|
+
req.on("error", (error) => rejectOnce(error instanceof Error ? error : new Error(String(error))));
|
|
45
132
|
});
|
|
46
133
|
}
|
|
47
|
-
function normalizeToolResult(result) {
|
|
134
|
+
function normalizeToolResult(result, traceId) {
|
|
48
135
|
const text = result.content?.[0]?.text ?? "";
|
|
49
136
|
try {
|
|
50
137
|
const data = JSON.parse(text);
|
|
51
138
|
if (result.isError) {
|
|
52
139
|
const errorCode = data && typeof data === "object" && "error" in data ? String(data.error ?? "UNKNOWN") : "UNKNOWN";
|
|
53
140
|
const message = data && typeof data === "object" && "message" in data ? String(data.message ?? "Error") : "Error";
|
|
54
|
-
return {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
error: errorCode,
|
|
144
|
+
code: errorCode,
|
|
145
|
+
message,
|
|
146
|
+
trace_id: traceId
|
|
147
|
+
};
|
|
55
148
|
}
|
|
56
149
|
return { ok: true, data };
|
|
57
150
|
} catch {
|
|
58
151
|
if (result.isError) {
|
|
59
|
-
return {
|
|
152
|
+
return {
|
|
153
|
+
ok: false,
|
|
154
|
+
error: "UNKNOWN",
|
|
155
|
+
code: "UNKNOWN",
|
|
156
|
+
message: text || "Error",
|
|
157
|
+
trace_id: traceId
|
|
158
|
+
};
|
|
60
159
|
}
|
|
61
160
|
return { ok: true, data: text };
|
|
62
161
|
}
|
|
@@ -77,132 +176,215 @@ function parseQueryParams(url) {
|
|
|
77
176
|
}
|
|
78
177
|
return params;
|
|
79
178
|
}
|
|
80
|
-
|
|
179
|
+
function enforceHttpSecurity(req, options) {
|
|
180
|
+
const allowedHosts = buildAllowedHosts(options.host, options.allowedHosts);
|
|
181
|
+
const hostHeader = normalizeHostHeader(req.headers.host);
|
|
182
|
+
if (allowedHosts.size > 0) {
|
|
183
|
+
if (!hostHeader || !allowedHosts.has(hostHeader)) {
|
|
184
|
+
throw new HttpRequestError(403, "INVALID_HOST", "Host header is not allowed");
|
|
185
|
+
}
|
|
186
|
+
const originHeader = req.headers.origin;
|
|
187
|
+
if (originHeader) {
|
|
188
|
+
let origin;
|
|
189
|
+
try {
|
|
190
|
+
origin = new URL(originHeader);
|
|
191
|
+
} catch {
|
|
192
|
+
throw new HttpRequestError(403, "INVALID_ORIGIN", "Origin header is invalid");
|
|
193
|
+
}
|
|
194
|
+
if (!allowedHosts.has(normalizeHost(origin.hostname))) {
|
|
195
|
+
throw new HttpRequestError(403, "INVALID_ORIGIN", "Origin header is not allowed");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (options.apiToken) {
|
|
200
|
+
const authHeader = req.headers.authorization;
|
|
201
|
+
const expected = `Bearer ${options.apiToken}`;
|
|
202
|
+
if (authHeader !== expected) {
|
|
203
|
+
throw new HttpRequestError(401, "UNAUTHORIZED", "Valid bearer token is required");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function handleRestTool(db, name, args, traceId) {
|
|
81
208
|
const result = await handleMcpRequest(db, name, args);
|
|
82
|
-
return normalizeToolResult(result);
|
|
209
|
+
return normalizeToolResult(result, traceId);
|
|
83
210
|
}
|
|
84
|
-
function createHttpServer(db) {
|
|
211
|
+
function createHttpServer(db, options = {}) {
|
|
212
|
+
const host = options.host ?? "127.0.0.1";
|
|
213
|
+
const mcpServer = new McpServer(db);
|
|
85
214
|
return http.createServer(async (req, res) => {
|
|
215
|
+
const traceId = getTraceId(req);
|
|
86
216
|
try {
|
|
87
217
|
const method = req.method ?? "GET";
|
|
88
218
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
89
219
|
if (method === "GET" && url.pathname === "/health") {
|
|
90
|
-
sendJson(res, 200, { ok: true, data: { status: "ok" } });
|
|
220
|
+
sendJson(res, 200, { ok: true, data: { status: "ok" } }, traceId);
|
|
91
221
|
return;
|
|
92
222
|
}
|
|
223
|
+
enforceHttpSecurity(req, {
|
|
224
|
+
host,
|
|
225
|
+
allowedHosts: options.allowedHosts,
|
|
226
|
+
apiToken: options.apiToken
|
|
227
|
+
});
|
|
93
228
|
if (method === "GET" && url.pathname === "/v1/tools") {
|
|
94
|
-
sendJson(res, 200, { ok: true, data: { tools: getMcpTools() } });
|
|
229
|
+
sendJson(res, 200, { ok: true, data: { tools: getMcpTools() } }, traceId);
|
|
95
230
|
return;
|
|
96
231
|
}
|
|
97
232
|
if (method === "POST" && url.pathname === "/v1/tools/call") {
|
|
98
|
-
const body = await
|
|
233
|
+
const body = await readJsonBody(req);
|
|
99
234
|
if (!body?.name) {
|
|
100
|
-
|
|
235
|
+
sendHttpError(res, 400, "INVALID_INPUT", "name is required", traceId);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const response = await handleRestTool(db, body.name, body.args ?? {}, traceId);
|
|
239
|
+
sendJson(res, response.ok ? 200 : 400, response, traceId);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (method === "POST" && url.pathname === "/mcp") {
|
|
243
|
+
const body = await readJsonBody(req);
|
|
244
|
+
const validation = JsonRpcRequestSchema.safeParse(body);
|
|
245
|
+
if (!validation.success) {
|
|
246
|
+
sendJsonRpcError(res, 400, null, -32600, "Invalid Request", traceId);
|
|
101
247
|
return;
|
|
102
248
|
}
|
|
103
|
-
const response = await
|
|
104
|
-
|
|
249
|
+
const response = await mcpServer.handleRequest(validation.data);
|
|
250
|
+
sendJsonRpcResponse(res, 200, response, traceId);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (method === "GET" && url.pathname === "/mcp") {
|
|
254
|
+
sendHttpError(
|
|
255
|
+
res,
|
|
256
|
+
501,
|
|
257
|
+
"STREAM_NOT_SUPPORTED",
|
|
258
|
+
"This server exposes MCP JSON-RPC over HTTP POST at /mcp. SSE streaming is not implemented.",
|
|
259
|
+
traceId
|
|
260
|
+
);
|
|
105
261
|
return;
|
|
106
262
|
}
|
|
107
263
|
if (method === "POST" && url.pathname === "/v1/sessions/start") {
|
|
108
|
-
const body = await
|
|
109
|
-
const response = await
|
|
110
|
-
sendJson(res, response.ok ? 200 : 400, response);
|
|
264
|
+
const body = await readJsonBody(req);
|
|
265
|
+
const response = await handleRestTool(db, "collab_session_start", body ?? {}, traceId);
|
|
266
|
+
sendJson(res, response.ok ? 200 : 400, response, traceId);
|
|
111
267
|
return;
|
|
112
268
|
}
|
|
113
269
|
if (method === "POST" && url.pathname === "/v1/sessions/end") {
|
|
114
|
-
const body = await
|
|
115
|
-
const response = await
|
|
116
|
-
sendJson(res, response.ok ? 200 : 400, response);
|
|
270
|
+
const body = await readJsonBody(req);
|
|
271
|
+
const response = await handleRestTool(db, "collab_session_end", body ?? {}, traceId);
|
|
272
|
+
sendJson(res, response.ok ? 200 : 400, response, traceId);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (method === "POST" && url.pathname === "/v1/sessions/update") {
|
|
276
|
+
const body = await readJsonBody(req);
|
|
277
|
+
const response = await handleRestTool(db, "collab_session_update", body ?? {}, traceId);
|
|
278
|
+
sendJson(res, response.ok ? 200 : 400, response, traceId);
|
|
117
279
|
return;
|
|
118
280
|
}
|
|
119
281
|
if (method === "GET" && url.pathname === "/v1/sessions") {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
sendJson(res, response.ok ? 200 : 400, response);
|
|
282
|
+
const response = await handleRestTool(db, "collab_session_list", parseQueryParams(url), traceId);
|
|
283
|
+
sendJson(res, response.ok ? 200 : 400, response, traceId);
|
|
123
284
|
return;
|
|
124
285
|
}
|
|
125
286
|
if (method === "POST" && url.pathname === "/v1/config") {
|
|
126
|
-
const body = await
|
|
127
|
-
const response = await
|
|
128
|
-
sendJson(res, response.ok ? 200 : 400, response);
|
|
287
|
+
const body = await readJsonBody(req);
|
|
288
|
+
const response = await handleRestTool(db, "collab_config", body ?? {}, traceId);
|
|
289
|
+
sendJson(res, response.ok ? 200 : 400, response, traceId);
|
|
129
290
|
return;
|
|
130
291
|
}
|
|
131
292
|
if (method === "GET" && url.pathname === "/v1/status") {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
sendJson(res, response.ok ? 200 : 400, response);
|
|
293
|
+
const response = await handleRestTool(db, "collab_status", parseQueryParams(url), traceId);
|
|
294
|
+
sendJson(res, response.ok ? 200 : 400, response, traceId);
|
|
135
295
|
return;
|
|
136
296
|
}
|
|
137
297
|
if (method === "POST" && url.pathname === "/v1/claims") {
|
|
138
|
-
const body = await
|
|
139
|
-
const response = await
|
|
140
|
-
sendJson(res, response.ok ? 200 : 400, response);
|
|
298
|
+
const body = await readJsonBody(req);
|
|
299
|
+
const response = await handleRestTool(db, "collab_claim", { action: "create", ...body ?? {} }, traceId);
|
|
300
|
+
sendJson(res, response.ok ? 200 : 400, response, traceId);
|
|
141
301
|
return;
|
|
142
302
|
}
|
|
143
303
|
if (method === "POST" && url.pathname === "/v1/claims/check") {
|
|
144
|
-
const body = await
|
|
145
|
-
const response = await
|
|
146
|
-
sendJson(res, response.ok ? 200 : 400, response);
|
|
304
|
+
const body = await readJsonBody(req);
|
|
305
|
+
const response = await handleRestTool(db, "collab_claim", { action: "check", ...body ?? {} }, traceId);
|
|
306
|
+
sendJson(res, response.ok ? 200 : 400, response, traceId);
|
|
147
307
|
return;
|
|
148
308
|
}
|
|
149
309
|
if (method === "POST" && url.pathname === "/v1/claims/release") {
|
|
150
|
-
const body = await
|
|
151
|
-
const response = await
|
|
152
|
-
sendJson(res, response.ok ? 200 : 400, response);
|
|
310
|
+
const body = await readJsonBody(req);
|
|
311
|
+
const response = await handleRestTool(db, "collab_claim", { action: "release", ...body ?? {} }, traceId);
|
|
312
|
+
sendJson(res, response.ok ? 200 : 400, response, traceId);
|
|
153
313
|
return;
|
|
154
314
|
}
|
|
155
315
|
if (method === "GET" && url.pathname === "/v1/claims") {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
316
|
+
const response = await handleRestTool(
|
|
317
|
+
db,
|
|
318
|
+
"collab_claim",
|
|
319
|
+
{ action: "list", ...parseQueryParams(url) },
|
|
320
|
+
traceId
|
|
321
|
+
);
|
|
322
|
+
sendJson(res, response.ok ? 200 : 400, response, traceId);
|
|
159
323
|
return;
|
|
160
324
|
}
|
|
161
325
|
if (method === "POST" && url.pathname === "/v1/memory/save") {
|
|
162
|
-
const body = await
|
|
163
|
-
const response = await
|
|
164
|
-
sendJson(res, response.ok ? 200 : 400, response);
|
|
326
|
+
const body = await readJsonBody(req);
|
|
327
|
+
const response = await handleRestTool(db, "collab_memory_save", body ?? {}, traceId);
|
|
328
|
+
sendJson(res, response.ok ? 200 : 400, response, traceId);
|
|
165
329
|
return;
|
|
166
330
|
}
|
|
167
331
|
if (method === "POST" && url.pathname === "/v1/memory/recall") {
|
|
168
|
-
const body = await
|
|
169
|
-
const response = await
|
|
170
|
-
sendJson(res, response.ok ? 200 : 400, response);
|
|
332
|
+
const body = await readJsonBody(req);
|
|
333
|
+
const response = await handleRestTool(db, "collab_memory_recall", body ?? {}, traceId);
|
|
334
|
+
sendJson(res, response.ok ? 200 : 400, response, traceId);
|
|
171
335
|
return;
|
|
172
336
|
}
|
|
173
337
|
if (method === "POST" && url.pathname === "/v1/memory/clear") {
|
|
174
|
-
const body = await
|
|
175
|
-
const response = await
|
|
176
|
-
sendJson(res, response.ok ? 200 : 400, response);
|
|
338
|
+
const body = await readJsonBody(req);
|
|
339
|
+
const response = await handleRestTool(db, "collab_memory_clear", body ?? {}, traceId);
|
|
340
|
+
sendJson(res, response.ok ? 200 : 400, response, traceId);
|
|
177
341
|
return;
|
|
178
342
|
}
|
|
179
343
|
if (method === "POST" && url.pathname === "/v1/protect/register") {
|
|
180
|
-
const body = await
|
|
181
|
-
const response = await
|
|
182
|
-
sendJson(res, response.ok ? 200 : 400, response);
|
|
344
|
+
const body = await readJsonBody(req);
|
|
345
|
+
const response = await handleRestTool(db, "collab_protect", { action: "register", ...body ?? {} }, traceId);
|
|
346
|
+
sendJson(res, response.ok ? 200 : 400, response, traceId);
|
|
183
347
|
return;
|
|
184
348
|
}
|
|
185
349
|
if (method === "POST" && url.pathname === "/v1/protect/check") {
|
|
186
|
-
const body = await
|
|
187
|
-
const response = await
|
|
188
|
-
sendJson(res, response.ok ? 200 : 400, response);
|
|
350
|
+
const body = await readJsonBody(req);
|
|
351
|
+
const response = await handleRestTool(db, "collab_protect", { action: "check", ...body ?? {} }, traceId);
|
|
352
|
+
sendJson(res, response.ok ? 200 : 400, response, traceId);
|
|
189
353
|
return;
|
|
190
354
|
}
|
|
191
355
|
if (method === "GET" && url.pathname === "/v1/protect/list") {
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
356
|
+
const response = await handleRestTool(
|
|
357
|
+
db,
|
|
358
|
+
"collab_protect",
|
|
359
|
+
{ action: "list", ...parseQueryParams(url) },
|
|
360
|
+
traceId
|
|
361
|
+
);
|
|
362
|
+
sendJson(res, response.ok ? 200 : 400, response, traceId);
|
|
195
363
|
return;
|
|
196
364
|
}
|
|
197
|
-
|
|
365
|
+
sendHttpError(res, 404, "NOT_FOUND", "Not found", traceId);
|
|
198
366
|
} catch (error) {
|
|
367
|
+
if (error instanceof HttpRequestError) {
|
|
368
|
+
sendHttpError(res, error.status, error.code, error.message, traceId);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
199
371
|
const message = error instanceof Error ? error.message : "Unexpected error";
|
|
200
|
-
|
|
372
|
+
sendHttpError(res, 500, "INTERNAL_ERROR", message, traceId);
|
|
201
373
|
}
|
|
202
374
|
});
|
|
203
375
|
}
|
|
204
376
|
async function startHttpServer(db, options) {
|
|
205
|
-
const
|
|
377
|
+
const normalizedHost = normalizeHost(options.host);
|
|
378
|
+
if (!isLocalBindHost(normalizedHost)) {
|
|
379
|
+
if (!options.apiToken) {
|
|
380
|
+
throw new Error("Non-local HTTP binds require SESSION_COLLAB_HTTP_TOKEN");
|
|
381
|
+
}
|
|
382
|
+
const allowedHosts = buildAllowedHosts(normalizedHost, options.allowedHosts);
|
|
383
|
+
if (allowedHosts.size === 0) {
|
|
384
|
+
throw new Error("Non-local HTTP binds require at least one allowed host");
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const server = createHttpServer(db, options);
|
|
206
388
|
await new Promise((resolve, reject) => {
|
|
207
389
|
server.listen(options.port, options.host, () => resolve());
|
|
208
390
|
server.on("error", reject);
|
|
@@ -214,20 +396,14 @@ async function startHttpServer(db, options) {
|
|
|
214
396
|
var __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
215
397
|
function loadMigrations() {
|
|
216
398
|
const migrationsDir = join(__dirname2, "..", "..", "migrations");
|
|
217
|
-
return
|
|
218
|
-
readFileSync(join(migrationsDir, "0001_init.sql"), "utf-8"),
|
|
219
|
-
readFileSync(join(migrationsDir, "0002_auth.sql"), "utf-8"),
|
|
220
|
-
readFileSync(join(migrationsDir, "0003_config.sql"), "utf-8"),
|
|
221
|
-
readFileSync(join(migrationsDir, "0004_symbols.sql"), "utf-8"),
|
|
222
|
-
readFileSync(join(migrationsDir, "0005_references.sql"), "utf-8"),
|
|
223
|
-
readFileSync(join(migrationsDir, "0006_composite_indexes.sql"), "utf-8")
|
|
224
|
-
];
|
|
399
|
+
return loadMigrationsFromDir(migrationsDir);
|
|
225
400
|
}
|
|
226
401
|
function parseArgs() {
|
|
227
402
|
const args = process.argv.slice(2);
|
|
228
403
|
let dbPath;
|
|
229
404
|
let host = "127.0.0.1";
|
|
230
405
|
let port = 8765;
|
|
406
|
+
const allowedHosts = [];
|
|
231
407
|
for (let i = 0; i < args.length; i++) {
|
|
232
408
|
const value = args[i + 1];
|
|
233
409
|
if (args[i] === "--db" && value) {
|
|
@@ -239,13 +415,18 @@ function parseArgs() {
|
|
|
239
415
|
} else if (args[i] === "--port" && value) {
|
|
240
416
|
port = Number(value);
|
|
241
417
|
i++;
|
|
418
|
+
} else if (args[i] === "--allowed-host" && value) {
|
|
419
|
+
allowedHosts.push(value);
|
|
420
|
+
i++;
|
|
242
421
|
}
|
|
243
422
|
}
|
|
244
|
-
return { dbPath, host, port };
|
|
423
|
+
return { dbPath, host, port, allowedHosts };
|
|
245
424
|
}
|
|
246
425
|
async function main() {
|
|
247
|
-
const { dbPath, host, port } = parseArgs();
|
|
426
|
+
const { dbPath, host, port, allowedHosts } = parseArgs();
|
|
248
427
|
const db = createLocalDatabase(dbPath);
|
|
428
|
+
const apiToken = process.env.SESSION_COLLAB_HTTP_TOKEN;
|
|
429
|
+
const envAllowedHosts = (process.env.SESSION_COLLAB_ALLOWED_HOSTS ?? "").split(",").map((hostEntry) => hostEntry.trim()).filter(Boolean);
|
|
249
430
|
try {
|
|
250
431
|
const migrations = loadMigrations();
|
|
251
432
|
db.initSchema(migrations);
|
|
@@ -254,9 +435,16 @@ async function main() {
|
|
|
254
435
|
console.error("Warning: Migration error:", error);
|
|
255
436
|
}
|
|
256
437
|
}
|
|
257
|
-
await startHttpServer(db, {
|
|
438
|
+
await startHttpServer(db, {
|
|
439
|
+
host,
|
|
440
|
+
port,
|
|
441
|
+
apiToken,
|
|
442
|
+
allowedHosts: [...envAllowedHosts, ...allowedHosts]
|
|
443
|
+
});
|
|
258
444
|
console.error(`Session Collab HTTP Server running at http://${host}:${port}`);
|
|
259
445
|
console.error(`Database: ${dbPath ?? getDefaultDbPath()}`);
|
|
446
|
+
console.error(`MCP endpoint: POST /mcp`);
|
|
447
|
+
console.error(`Convenience REST API: /v1/*`);
|
|
260
448
|
}
|
|
261
449
|
main().catch((error) => {
|
|
262
450
|
console.error("Fatal error:", error);
|