session-collab-mcp 2.1.0 → 2.2.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-SOUW3JSS.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-SOUW3JSS.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,209 @@ 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);
117
273
  return;
118
274
  }
119
275
  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);
276
+ const response = await handleRestTool(db, "collab_session_list", parseQueryParams(url), traceId);
277
+ sendJson(res, response.ok ? 200 : 400, response, traceId);
123
278
  return;
124
279
  }
125
280
  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);
281
+ const body = await readJsonBody(req);
282
+ const response = await handleRestTool(db, "collab_config", body ?? {}, traceId);
283
+ sendJson(res, response.ok ? 200 : 400, response, traceId);
129
284
  return;
130
285
  }
131
286
  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);
287
+ const response = await handleRestTool(db, "collab_status", parseQueryParams(url), traceId);
288
+ sendJson(res, response.ok ? 200 : 400, response, traceId);
135
289
  return;
136
290
  }
137
291
  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);
292
+ const body = await readJsonBody(req);
293
+ const response = await handleRestTool(db, "collab_claim", { action: "create", ...body ?? {} }, traceId);
294
+ sendJson(res, response.ok ? 200 : 400, response, traceId);
141
295
  return;
142
296
  }
143
297
  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);
298
+ const body = await readJsonBody(req);
299
+ const response = await handleRestTool(db, "collab_claim", { action: "check", ...body ?? {} }, traceId);
300
+ sendJson(res, response.ok ? 200 : 400, response, traceId);
147
301
  return;
148
302
  }
149
303
  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);
304
+ const body = await readJsonBody(req);
305
+ const response = await handleRestTool(db, "collab_claim", { action: "release", ...body ?? {} }, traceId);
306
+ sendJson(res, response.ok ? 200 : 400, response, traceId);
153
307
  return;
154
308
  }
155
309
  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);
310
+ const response = await handleRestTool(
311
+ db,
312
+ "collab_claim",
313
+ { action: "list", ...parseQueryParams(url) },
314
+ traceId
315
+ );
316
+ sendJson(res, response.ok ? 200 : 400, response, traceId);
159
317
  return;
160
318
  }
161
319
  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);
320
+ const body = await readJsonBody(req);
321
+ const response = await handleRestTool(db, "collab_memory_save", body ?? {}, traceId);
322
+ sendJson(res, response.ok ? 200 : 400, response, traceId);
165
323
  return;
166
324
  }
167
325
  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);
326
+ const body = await readJsonBody(req);
327
+ const response = await handleRestTool(db, "collab_memory_recall", body ?? {}, traceId);
328
+ sendJson(res, response.ok ? 200 : 400, response, traceId);
171
329
  return;
172
330
  }
173
331
  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);
332
+ const body = await readJsonBody(req);
333
+ const response = await handleRestTool(db, "collab_memory_clear", body ?? {}, traceId);
334
+ sendJson(res, response.ok ? 200 : 400, response, traceId);
177
335
  return;
178
336
  }
179
337
  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);
338
+ const body = await readJsonBody(req);
339
+ const response = await handleRestTool(db, "collab_protect", { action: "register", ...body ?? {} }, traceId);
340
+ sendJson(res, response.ok ? 200 : 400, response, traceId);
183
341
  return;
184
342
  }
185
343
  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);
344
+ const body = await readJsonBody(req);
345
+ const response = await handleRestTool(db, "collab_protect", { action: "check", ...body ?? {} }, traceId);
346
+ sendJson(res, response.ok ? 200 : 400, response, traceId);
189
347
  return;
190
348
  }
191
349
  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);
350
+ const response = await handleRestTool(
351
+ db,
352
+ "collab_protect",
353
+ { action: "list", ...parseQueryParams(url) },
354
+ traceId
355
+ );
356
+ sendJson(res, response.ok ? 200 : 400, response, traceId);
195
357
  return;
196
358
  }
197
- sendJson(res, 404, { ok: false, error: { code: "NOT_FOUND", message: "Not found" } });
359
+ sendHttpError(res, 404, "NOT_FOUND", "Not found", traceId);
198
360
  } catch (error) {
361
+ if (error instanceof HttpRequestError) {
362
+ sendHttpError(res, error.status, error.code, error.message, traceId);
363
+ return;
364
+ }
199
365
  const message = error instanceof Error ? error.message : "Unexpected error";
200
- sendJson(res, 500, { ok: false, error: { code: "INTERNAL_ERROR", message } });
366
+ sendHttpError(res, 500, "INTERNAL_ERROR", message, traceId);
201
367
  }
202
368
  });
203
369
  }
204
370
  async function startHttpServer(db, options) {
205
- const server = createHttpServer(db);
371
+ const normalizedHost = normalizeHost(options.host);
372
+ if (!isLocalBindHost(normalizedHost)) {
373
+ if (!options.apiToken) {
374
+ throw new Error("Non-local HTTP binds require SESSION_COLLAB_HTTP_TOKEN");
375
+ }
376
+ const allowedHosts = buildAllowedHosts(normalizedHost, options.allowedHosts);
377
+ if (allowedHosts.size === 0) {
378
+ throw new Error("Non-local HTTP binds require at least one allowed host");
379
+ }
380
+ }
381
+ const server = createHttpServer(db, options);
206
382
  await new Promise((resolve, reject) => {
207
383
  server.listen(options.port, options.host, () => resolve());
208
384
  server.on("error", reject);
@@ -214,20 +390,14 @@ async function startHttpServer(db, options) {
214
390
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
215
391
  function loadMigrations() {
216
392
  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
- ];
393
+ return loadMigrationsFromDir(migrationsDir);
225
394
  }
226
395
  function parseArgs() {
227
396
  const args = process.argv.slice(2);
228
397
  let dbPath;
229
398
  let host = "127.0.0.1";
230
399
  let port = 8765;
400
+ const allowedHosts = [];
231
401
  for (let i = 0; i < args.length; i++) {
232
402
  const value = args[i + 1];
233
403
  if (args[i] === "--db" && value) {
@@ -239,13 +409,18 @@ function parseArgs() {
239
409
  } else if (args[i] === "--port" && value) {
240
410
  port = Number(value);
241
411
  i++;
412
+ } else if (args[i] === "--allowed-host" && value) {
413
+ allowedHosts.push(value);
414
+ i++;
242
415
  }
243
416
  }
244
- return { dbPath, host, port };
417
+ return { dbPath, host, port, allowedHosts };
245
418
  }
246
419
  async function main() {
247
- const { dbPath, host, port } = parseArgs();
420
+ const { dbPath, host, port, allowedHosts } = parseArgs();
248
421
  const db = createLocalDatabase(dbPath);
422
+ const apiToken = process.env.SESSION_COLLAB_HTTP_TOKEN;
423
+ const envAllowedHosts = (process.env.SESSION_COLLAB_ALLOWED_HOSTS ?? "").split(",").map((hostEntry) => hostEntry.trim()).filter(Boolean);
249
424
  try {
250
425
  const migrations = loadMigrations();
251
426
  db.initSchema(migrations);
@@ -254,9 +429,16 @@ async function main() {
254
429
  console.error("Warning: Migration error:", error);
255
430
  }
256
431
  }
257
- await startHttpServer(db, { host, port });
432
+ await startHttpServer(db, {
433
+ host,
434
+ port,
435
+ apiToken,
436
+ allowedHosts: [...envAllowedHosts, ...allowedHosts]
437
+ });
258
438
  console.error(`Session Collab HTTP Server running at http://${host}:${port}`);
259
439
  console.error(`Database: ${dbPath ?? getDefaultDbPath()}`);
440
+ console.error(`MCP endpoint: POST /mcp`);
441
+ console.error(`Convenience REST API: /v1/*`);
260
442
  }
261
443
  main().catch((error) => {
262
444
  console.error("Fatal error:", error);