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/dist/cli.js CHANGED
@@ -6,25 +6,18 @@ import {
6
6
  createLocalDatabase,
7
7
  getDefaultDbPath,
8
8
  getMcpTools,
9
- handleMcpRequest
10
- } from "./chunk-V4APNPXF.js";
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 { readFileSync } from 'fs';\nimport { join, dirname } from 'path';\nimport { fileURLToPath } from 'url';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n// Load migrations\nfunction loadMigrations(): string[] {\n const migrationsDir = join(__dirname, '..', 'migrations');\n return [\n readFileSync(join(migrationsDir, '0001_init.sql'), 'utf-8'),\n readFileSync(join(migrationsDir, '0002_auth.sql'), 'utf-8'),\n readFileSync(join(migrationsDir, '0003_config.sql'), 'utf-8'),\n readFileSync(join(migrationsDir, '0004_symbols.sql'), 'utf-8'),\n readFileSync(join(migrationsDir, '0005_references.sql'), 'utf-8'),\n readFileSync(join(migrationsDir, '0006_composite_indexes.sql'), 'utf-8'),\n ];\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":[]}
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
- } from "../chunk-V4APNPXF.js";
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
- function sendJson(res, status, body) {
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": "application/json",
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 parseBody(req) {
26
- return new Promise((resolve, reject) => {
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
- reject(new Error("Payload too large"));
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
- resolve(void 0);
122
+ resolveOnce(void 0);
37
123
  return;
38
124
  }
39
125
  try {
40
- resolve(JSON.parse(data));
41
- } catch (error) {
42
- reject(error);
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 { ok: false, error: { code: errorCode, message } };
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 { ok: false, error: { code: "UNKNOWN", message: text || "Error" } };
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
- async function handleTool(db, name, args) {
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 parseBody(req);
233
+ const body = await readJsonBody(req);
99
234
  if (!body?.name) {
100
- sendJson(res, 400, { ok: false, error: { code: "INVALID_INPUT", message: "name is required" } });
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 handleTool(db, body.name, body.args ?? {});
104
- sendJson(res, response.ok ? 200 : 400, response);
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 parseBody(req);
109
- const response = await handleTool(db, "collab_session_start", body ?? {});
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 parseBody(req);
115
- const response = await handleTool(db, "collab_session_end", body ?? {});
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 args = parseQueryParams(url);
121
- const response = await handleTool(db, "collab_session_list", args);
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 parseBody(req);
127
- const response = await handleTool(db, "collab_config", body ?? {});
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 args = parseQueryParams(url);
133
- const response = await handleTool(db, "collab_status", args);
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 parseBody(req);
139
- const response = await handleTool(db, "collab_claim", { action: "create", ...body ?? {} });
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 parseBody(req);
145
- const response = await handleTool(db, "collab_claim", { action: "check", ...body ?? {} });
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 parseBody(req);
151
- const response = await handleTool(db, "collab_claim", { action: "release", ...body ?? {} });
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 args = parseQueryParams(url);
157
- const response = await handleTool(db, "collab_claim", { action: "list", ...args });
158
- sendJson(res, response.ok ? 200 : 400, response);
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 parseBody(req);
163
- const response = await handleTool(db, "collab_memory_save", body ?? {});
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 parseBody(req);
169
- const response = await handleTool(db, "collab_memory_recall", body ?? {});
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 parseBody(req);
175
- const response = await handleTool(db, "collab_memory_clear", body ?? {});
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 parseBody(req);
181
- const response = await handleTool(db, "collab_protect", { action: "register", ...body ?? {} });
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 parseBody(req);
187
- const response = await handleTool(db, "collab_protect", { action: "check", ...body ?? {} });
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 args = parseQueryParams(url);
193
- const response = await handleTool(db, "collab_protect", { action: "list", ...args });
194
- sendJson(res, response.ok ? 200 : 400, response);
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
- sendJson(res, 404, { ok: false, error: { code: "NOT_FOUND", message: "Not found" } });
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
- sendJson(res, 500, { ok: false, error: { code: "INTERNAL_ERROR", message } });
372
+ sendHttpError(res, 500, "INTERNAL_ERROR", message, traceId);
201
373
  }
202
374
  });
203
375
  }
204
376
  async function startHttpServer(db, options) {
205
- const server = createHttpServer(db);
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, { host, port });
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);