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/README.md +90 -50
- package/dist/{chunk-V4APNPXF.js → chunk-SOUW3JSS.js} +120 -9
- package/dist/chunk-SOUW3JSS.js.map +1 -0
- package/dist/cli.js +4 -11
- package/dist/cli.js.map +1 -1
- package/dist/http/cli.js +266 -84
- package/dist/http/cli.js.map +1 -1
- package/package.json +11 -6
- 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-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 {
|
|
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":";;;;;;;;;;;;;AAIA,SAAS,uBAAuB;AAKhC,SAAS,MAAM,eAAe;AAC9B,SAAS,qBAAqB;AAE9B,IAAM,YAAY,QAAQ,cAAc,YAAY,GAAG,CAAC;AAIxD,SAAS,iBAA2B;AAClC,QAAM,gBAAgB,KAAK,WAAW,MAAM,YAAY;AACxD,SAAO,sBAAsB,aAAa;AAC5C;AAGA,SAAS,YAAiC;AACxC,QAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AACjC,MAAI;AAEJ,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,QAAI,KAAK,CAAC,MAAM,UAAU,KAAK,IAAI,CAAC,GAAG;AACrC,eAAS,KAAK,IAAI,CAAC;AACnB;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,OAAO;AAClB;AAGA,SAAS,gBAAgB,IAA4B,QAAyB;AAC5E,SAAO,KAAK,UAAU,EAAE,SAAS,OAAO,IAAI,OAAO,CAAC;AACtD;AAEA,SAAS,aAAa,IAA4B,MAAc,SAAyB;AACvF,SAAO,KAAK,UAAU,EAAE,SAAS,OAAO,IAAI,OAAO,EAAE,MAAM,QAAQ,EAAE,CAAC;AACxE;AAEA,eAAe,OAAsB;AACnC,QAAM,EAAE,OAAO,IAAI,UAAU;AAC7B,QAAM,KAAK,oBAAoB,MAAM;AAGrC,MAAI;AACF,UAAM,aAAa,eAAe;AAClC,OAAG,WAAW,UAAU;AAAA,EAC1B,SAAS,OAAO;AAEd,QAAI,EAAE,iBAAiB,SAAS,MAAM,QAAQ,SAAS,gBAAgB,IAAI;AACzE,cAAQ,MAAM,6BAA6B,KAAK;AAAA,IAClD;AAAA,EACF;AAGA,UAAQ,MAAM,mCAAmC;AACjD,UAAQ,MAAM,aAAa,UAAU,iBAAiB,CAAC,EAAE;AAEzD,QAAM,KAAK,gBAAgB;AAAA,IACzB,OAAO,QAAQ;AAAA,IACf,QAAQ,QAAQ;AAAA,IAChB,UAAU;AAAA,EACZ,CAAC;AAED,KAAG,GAAG,QAAQ,OAAO,SAAS;AAC5B,QAAI,CAAC,KAAK,KAAK,EAAG;AAElB,QAAI;AACF,YAAM,UAAU,KAAK,MAAM,IAAI;AAC/B,YAAM,EAAE,IAAI,QAAQ,OAAO,IAAI;AAG/B,cAAQ,QAAQ;AAAA,QACd,KAAK,cAAc;AACjB,gBAAM,WAAW,gBAAgB,IAAI;AAAA,YACnC,iBAAiB;AAAA,YACjB,cAAc;AAAA,cACZ,OAAO,CAAC;AAAA,YACV;AAAA,YACA,YAAY;AAAA,cACV,MAAM;AAAA,cACN,SAAS;AAAA,YACX;AAAA,YACA,cAAc;AAAA,UAChB,CAAC;AACD,kBAAQ,IAAI,QAAQ;AACpB;AAAA,QACF;AAAA,QAEA,KAAK,6BAA6B;AAEhC;AAAA,QACF;AAAA,QAEA,KAAK,cAAc;AACjB,gBAAM,QAAQ,YAAY;AAC1B,gBAAM,WAAW,gBAAgB,IAAI,EAAE,MAAM,CAAC;AAC9C,kBAAQ,IAAI,QAAQ;AACpB;AAAA,QACF;AAAA,QAEA,KAAK,cAAc;AACjB,gBAAM,SAAS,MAAM;AAAA,YACnB;AAAA,YACA,OAAO;AAAA,YACP,OAAO,aAAa,CAAC;AAAA,UACvB;AACA,gBAAM,WAAW,gBAAgB,IAAI,MAAM;AAC3C,kBAAQ,IAAI,QAAQ;AACpB;AAAA,QACF;AAAA,QAEA,SAAS;AACP,gBAAM,gBAAgB,aAAa,IAAI,QAAQ,qBAAqB,MAAM,EAAE;AAC5E,kBAAQ,IAAI,aAAa;AAAA,QAC3B;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AACd,YAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU;AAC9D,YAAM,gBAAgB,aAAa,MAAM,QAAQ,gBAAgB,YAAY,EAAE;AAC/E,cAAQ,IAAI,aAAa;AAAA,IAC3B;AAAA,EACF,CAAC;AAED,KAAG,GAAG,SAAS,MAAM;AACnB,OAAG,MAAM;AACT,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;AAEA,KAAK,EAAE,MAAM,CAAC,UAAU;AACtB,UAAQ,MAAM,gBAAgB,KAAK;AACnC,UAAQ,KAAK,CAAC;AAChB,CAAC;","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-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
|
-
|
|
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,209 @@ 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);
|
|
117
273
|
return;
|
|
118
274
|
}
|
|
119
275
|
if (method === "GET" && url.pathname === "/v1/sessions") {
|
|
120
|
-
const
|
|
121
|
-
|
|
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
|
|
127
|
-
const response = await
|
|
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
|
|
133
|
-
|
|
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
|
|
139
|
-
const response = await
|
|
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
|
|
145
|
-
const response = await
|
|
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
|
|
151
|
-
const response = await
|
|
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
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
163
|
-
const response = await
|
|
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
|
|
169
|
-
const response = await
|
|
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
|
|
175
|
-
const response = await
|
|
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
|
|
181
|
-
const response = await
|
|
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
|
|
187
|
-
const response = await
|
|
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
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
366
|
+
sendHttpError(res, 500, "INTERNAL_ERROR", message, traceId);
|
|
201
367
|
}
|
|
202
368
|
});
|
|
203
369
|
}
|
|
204
370
|
async function startHttpServer(db, options) {
|
|
205
|
-
const
|
|
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, {
|
|
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);
|