@gresmcp/mcp 1.0.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/mcp.js ADDED
@@ -0,0 +1,515 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/mcp.ts
4
+ import { FastMCP, UserError } from "fastmcp";
5
+ import { z } from "zod";
6
+
7
+ // src/db.ts
8
+ import { Pool } from "pg";
9
+ import { toSql } from "pgvector";
10
+ import { createHash } from "crypto";
11
+
12
+ // src/config.ts
13
+ var DEFAULT_DATABASE_URL = "postgresql://gres:gres@localhost/gres";
14
+ var DEFAULT_OLLAMA_URL = "http://localhost:11434";
15
+ function databaseUrl() {
16
+ return process.env.GRESMCP_DATABASE_URL || DEFAULT_DATABASE_URL;
17
+ }
18
+ function normalizeUrl(url) {
19
+ return url.replace(/\/+$/, "");
20
+ }
21
+
22
+ // src/db.ts
23
+ var pool;
24
+ function getPool() {
25
+ if (!pool) {
26
+ pool = new Pool({ connectionString: databaseUrl(), max: 5 });
27
+ }
28
+ return pool;
29
+ }
30
+ async function closePool() {
31
+ if (pool) {
32
+ await pool.end();
33
+ pool = void 0;
34
+ }
35
+ }
36
+ function entryTableName(ks) {
37
+ const name = `entry_${ks.id.replace(/-/g, "")}`;
38
+ assertTableName(name);
39
+ return name;
40
+ }
41
+ function assertTableName(name) {
42
+ if (!/^entry_[0-9a-f]{32}$/.test(name)) {
43
+ throw new Error(`Refusing to use unsafe table name: ${name}`);
44
+ }
45
+ }
46
+ async function initDb() {
47
+ const p = getPool();
48
+ await p.query(`CREATE EXTENSION IF NOT EXISTS vector`);
49
+ await p.query(`
50
+ CREATE TABLE IF NOT EXISTS ks (
51
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
52
+ name text NOT NULL UNIQUE,
53
+ description text NOT NULL DEFAULT '',
54
+ embedding_model text NOT NULL,
55
+ embedding_dim integer NOT NULL,
56
+ ollama_url text NOT NULL DEFAULT '${DEFAULT_OLLAMA_URL}',
57
+ created_at timestamptz NOT NULL DEFAULT now(),
58
+ updated_at timestamptz NOT NULL DEFAULT now()
59
+ )
60
+ `);
61
+ }
62
+ async function listKs() {
63
+ const res = await getPool().query(`SELECT * FROM ks ORDER BY name`);
64
+ return res.rows;
65
+ }
66
+ async function countEntries(ks) {
67
+ const tbl = entryTableName(ks);
68
+ const res = await getPool().query(`SELECT count(*)::int AS n FROM ${tbl}`);
69
+ return Number(res.rows[0]?.n ?? 0);
70
+ }
71
+
72
+ // src/search.ts
73
+ import { toSql as toSql2 } from "pgvector";
74
+
75
+ // src/ollama.ts
76
+ var OllamaError = class extends Error {
77
+ constructor(message, status) {
78
+ super(message);
79
+ this.status = status;
80
+ this.name = "OllamaError";
81
+ }
82
+ status;
83
+ };
84
+ async function embed(baseUrl, model, inputs, timeoutMs = 12e4) {
85
+ const url = `${normalizeUrl(baseUrl)}/api/embed`;
86
+ let res;
87
+ try {
88
+ res = await fetch(url, {
89
+ method: "POST",
90
+ headers: { "content-type": "application/json" },
91
+ body: JSON.stringify({ model, input: inputs }),
92
+ signal: AbortSignal.timeout(timeoutMs)
93
+ });
94
+ } catch (err) {
95
+ throw new OllamaError(
96
+ `Cannot reach Ollama at ${normalizeUrl(baseUrl)} (${err instanceof Error ? err.message : err})`
97
+ );
98
+ }
99
+ if (!res.ok) {
100
+ const body = await res.text().catch(() => "");
101
+ throw new OllamaError(`Ollama returned ${res.status} for ${url}: ${body.slice(0, 300)}`, res.status);
102
+ }
103
+ const data = await res.json();
104
+ if (data.error) {
105
+ throw new OllamaError(`Ollama error: ${data.error}`);
106
+ }
107
+ if (!Array.isArray(data.embeddings) || data.embeddings.length !== inputs.length) {
108
+ throw new OllamaError(`Unexpected response shape from ${url}`);
109
+ }
110
+ return data.embeddings;
111
+ }
112
+
113
+ // src/search.ts
114
+ var RRF_K = 60;
115
+ var HEADLINE_OPTS = "MaxWords=35, MinWords=15, ShortWord=0, MaxFragments=1, StartSel=[, StopSel=]";
116
+ function tagClause(params, tags) {
117
+ if (!tags || tags.length === 0) return "";
118
+ params.push(tags);
119
+ return ` AND metadata->'tags' ?| $${params.length}::text[]`;
120
+ }
121
+ function buildSearchSql(tbl, mode, limit, tags) {
122
+ const params = [];
123
+ if (mode === "vector") params.push("__VECTOR__");
124
+ else if (mode === "keyword") params.push("__QUERY__");
125
+ else params.push("__VECTOR__", "__QUERY__");
126
+ const tag = tagClause(params, tags);
127
+ if (mode === "vector") {
128
+ const sql2 = `
129
+ SELECT id, source, title, chunk_index, metadata,
130
+ 1 - (embedding <=> $1::vector) AS score,
131
+ left(regexp_replace(content, '\\s+', ' ', 'g'), 320) AS snippet
132
+ FROM ${tbl}
133
+ WHERE true${tag}
134
+ ORDER BY embedding <=> $1::vector
135
+ LIMIT ${limit}`;
136
+ return { sql: sql2, params };
137
+ }
138
+ if (mode === "keyword") {
139
+ const sql2 = `
140
+ WITH q AS (SELECT websearch_to_tsquery('english', $1) AS query)
141
+ SELECT e.id, e.source, e.title, e.chunk_index, e.metadata,
142
+ ts_rank_cd(e.tsv, q.query) AS score,
143
+ ts_headline('english', e.content, q.query, '${HEADLINE_OPTS}') AS snippet
144
+ FROM ${tbl} e CROSS JOIN q
145
+ WHERE e.tsv @@ q.query${tag}
146
+ ORDER BY score DESC, e.id
147
+ LIMIT ${limit}`;
148
+ return { sql: sql2, params };
149
+ }
150
+ const inner = Math.min(Math.max(limit * 4, 20), 100);
151
+ const sql = `
152
+ WITH v AS (
153
+ SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <=> $1::vector) AS r
154
+ FROM ${tbl}
155
+ WHERE true${tag}
156
+ ORDER BY embedding <=> $1::vector
157
+ LIMIT ${inner}
158
+ ),
159
+ k AS (
160
+ SELECT id, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(tsv, q.query) DESC) AS r
161
+ FROM ${tbl} CROSS JOIN (SELECT websearch_to_tsquery('english', $2) AS query) q
162
+ WHERE tsv @@ q.query${tag}
163
+ ORDER BY ts_rank_cd(tsv, q.query) DESC
164
+ LIMIT ${inner}
165
+ ),
166
+ s AS (
167
+ SELECT COALESCE(v.id, k.id) AS id,
168
+ COALESCE(1.0 / (${RRF_K} + v.r), 0) + COALESCE(1.0 / (${RRF_K} + k.r), 0) AS score,
169
+ (v.r IS NOT NULL) AS vhit,
170
+ (k.r IS NOT NULL) AS khit
171
+ FROM v FULL JOIN k ON k.id = v.id
172
+ )
173
+ SELECT e.id, e.source, e.title, e.chunk_index, e.metadata, s.score,
174
+ CASE WHEN s.khit
175
+ THEN ts_headline('english', e.content, websearch_to_tsquery('english', $2), '${HEADLINE_OPTS}')
176
+ ELSE left(regexp_replace(e.content, '\\s+', ' ', 'g'), 320)
177
+ END AS snippet
178
+ FROM s
179
+ JOIN ${tbl} e ON e.id = s.id
180
+ ORDER BY s.score DESC, e.id
181
+ LIMIT ${limit}`;
182
+ return { sql, params };
183
+ }
184
+ async function queryEmbedding(ks, query, cache) {
185
+ const key = `${normalizeUrl(ks.ollama_url)}::${ks.embedding_model}::${query}`;
186
+ const cached = cache.get(key);
187
+ if (cached) return cached;
188
+ const embs = await embed(ks.ollama_url, ks.embedding_model, [query], 3e4);
189
+ const emb = embs[0];
190
+ if (!emb) {
191
+ throw new Error(`Ollama returned an empty embedding for query on ks '${ks.name}'`);
192
+ }
193
+ cache.set(key, emb);
194
+ return emb;
195
+ }
196
+ async function search(ksList, args) {
197
+ const mode = args.mode ?? "hybrid";
198
+ const limit = Math.min(Math.max(args.limit ?? 8, 1), 50);
199
+ let targets = ksList;
200
+ if (args.ksFilter && args.ksFilter.length > 0) {
201
+ const wanted = new Set(args.ksFilter);
202
+ targets = ksList.filter((k) => wanted.has(k.name));
203
+ }
204
+ if (targets.length === 0) return [];
205
+ const cache = /* @__PURE__ */ new Map();
206
+ const all = [];
207
+ for (const ks of targets) {
208
+ const tbl = entryTableName(ks);
209
+ const { sql, params } = buildSearchSql(tbl, mode, limit, args.tags);
210
+ const resolved = [];
211
+ for (const p of params) {
212
+ if (p === "__VECTOR__") {
213
+ const emb = await queryEmbedding(ks, args.query, cache);
214
+ if (emb.length !== ks.embedding_dim) {
215
+ throw new Error(
216
+ `Embedding dimension mismatch for ks '${ks.name}': expected ${ks.embedding_dim}, got ${emb.length}`
217
+ );
218
+ }
219
+ resolved.push(toSql2(emb));
220
+ } else if (p === "__QUERY__") {
221
+ resolved.push(args.query);
222
+ } else {
223
+ resolved.push(p);
224
+ }
225
+ }
226
+ const res = await getPool().query(sql, resolved);
227
+ for (const row of res.rows) {
228
+ all.push({
229
+ ks: ks.name,
230
+ id: Number(row.id),
231
+ source: String(row.source),
232
+ title: String(row.title ?? ""),
233
+ chunk_index: Number(row.chunk_index ?? 0),
234
+ score: Number(row.score ?? 0),
235
+ snippet: String(row.snippet ?? ""),
236
+ metadata: row.metadata ?? {}
237
+ });
238
+ }
239
+ }
240
+ all.sort((a, b) => b.score - a.score || a.ks.localeCompare(b.ks));
241
+ return all.slice(0, limit);
242
+ }
243
+ async function getEntry(ks, id) {
244
+ const tbl = entryTableName(ks);
245
+ const res = await getPool().query(
246
+ `SELECT id, source, title, chunk_index, content, content_hash, metadata, created_at
247
+ FROM ${tbl} WHERE id = $1`,
248
+ [id]
249
+ );
250
+ return res.rows[0];
251
+ }
252
+ async function listEntries(ks, opts = {}) {
253
+ const tbl = entryTableName(ks);
254
+ const limit = Math.min(Math.max(opts.limit ?? 20, 1), 200);
255
+ const offset = Math.max(opts.offset ?? 0, 0);
256
+ const params = [];
257
+ let where = "";
258
+ if (opts.source) {
259
+ params.push(opts.source);
260
+ where = `WHERE source = $${params.length}`;
261
+ }
262
+ params.push(limit, offset);
263
+ const res = await getPool().query(
264
+ `SELECT id, source, title, chunk_index, length(content) AS chars, created_at
265
+ FROM ${tbl} ${where}
266
+ ORDER BY source, chunk_index, id
267
+ LIMIT $${params.length - 1} OFFSET $${params.length}`,
268
+ params
269
+ );
270
+ return res.rows.map((r) => ({
271
+ id: Number(r.id),
272
+ source: String(r.source),
273
+ title: String(r.title ?? ""),
274
+ chunk_index: Number(r.chunk_index),
275
+ chars: Number(r.chars),
276
+ created_at: r.created_at
277
+ }));
278
+ }
279
+ async function listSources(ksList) {
280
+ const out = [];
281
+ for (const ks of ksList) {
282
+ const tbl = entryTableName(ks);
283
+ const res = await getPool().query(
284
+ `SELECT source, max(title) AS title, count(*)::int AS chunks, sum(length(content))::bigint AS chars
285
+ FROM ${tbl}
286
+ GROUP BY source
287
+ ORDER BY source`
288
+ );
289
+ for (const r of res.rows) {
290
+ out.push({
291
+ ks: ks.name,
292
+ source: String(r.source),
293
+ title: String(r.title ?? ""),
294
+ chunks: Number(r.chunks),
295
+ chars: Number(r.chars)
296
+ });
297
+ }
298
+ }
299
+ return out;
300
+ }
301
+
302
+ // src/version.ts
303
+ var VERSION = "1.0.0";
304
+
305
+ // src/mcp.ts
306
+ function parseArgs(argv) {
307
+ const ksNames = [];
308
+ let all = false;
309
+ let transport = "stdio";
310
+ let port = 8080;
311
+ for (let i = 0; i < argv.length; i++) {
312
+ const arg = argv[i];
313
+ if (arg === "--all") {
314
+ all = true;
315
+ } else if (arg === "--transport" || arg === "-t") {
316
+ const value = argv[++i];
317
+ if (value !== "stdio" && value !== "http") {
318
+ throw new Error(`invalid --transport value '${value}', expected 'stdio' or 'http'`);
319
+ }
320
+ transport = value;
321
+ } else if (arg === "--port" || arg === "-p") {
322
+ port = parseInt(argv[++i] ?? "", 10);
323
+ if (!Number.isFinite(port) || port <= 0) throw new Error(`invalid --port value`);
324
+ } else if (arg === "--help" || arg === "-h") {
325
+ console.error(`usage: mcp [ks1,ks2,... | --all] [--transport stdio|http] [--port N]`);
326
+ process.exit(0);
327
+ } else if (arg.startsWith("-")) {
328
+ throw new Error(`unknown option '${arg}'`);
329
+ } else {
330
+ ksNames.push(...arg.split(",").map((s) => s.trim()).filter(Boolean));
331
+ }
332
+ }
333
+ if (!all && ksNames.length === 0) {
334
+ const env = process.env.GRESMCP_KS;
335
+ if (env) ksNames.push(...env.split(",").map((s) => s.trim()).filter(Boolean));
336
+ }
337
+ return { ksNames, all, transport, port };
338
+ }
339
+ async function main() {
340
+ const { ksNames, all, transport, port } = parseArgs(process.argv.slice(2));
341
+ await initDb();
342
+ const available = await listKs();
343
+ let scoped;
344
+ if (all) {
345
+ scoped = available;
346
+ } else {
347
+ const wanted = new Set(ksNames);
348
+ const missing = ksNames.filter((n) => !available.some((k) => k.name === n));
349
+ if (missing.length > 0) {
350
+ throw new Error(
351
+ `unknown knowledge source(s): ${missing.join(", ")}. Available: ${available.map((k) => k.name).join(", ") || "(none)"}`
352
+ );
353
+ }
354
+ scoped = available.filter((k) => wanted.has(k.name));
355
+ }
356
+ if (scoped.length === 0) {
357
+ throw new Error("no knowledge sources configured; pass ks names as arguments or use --all");
358
+ }
359
+ const descriptions = scoped.map((k) => `- ${k.name}: ${k.description || "(no description)"} [model=${k.embedding_model}, dim=${k.embedding_dim}]`).join("\n");
360
+ const server = new FastMCP({
361
+ name: "gresmcp",
362
+ version: VERSION,
363
+ instructions: `This server exposes read-only access to Postgres-backed knowledge sources (RAG: pgvector + tsvector hybrid search).
364
+ Available knowledge sources:
365
+ ${descriptions}
366
+ Use 'search' with hybrid mode for best results, 'get_entry' to read a full chunk, 'list_sources' to see what documents exist.`
367
+ });
368
+ const ksRef = z.string().optional().describe("Knowledge source name; optional when the server has exactly one");
369
+ function resolveKs(name) {
370
+ if (!name) {
371
+ if (scoped.length === 1) return scoped[0];
372
+ throw new UserError(
373
+ `Multiple knowledge sources configured (${scoped.map((k) => k.name).join(", ")}); specify one with the 'ks' parameter`
374
+ );
375
+ }
376
+ const ks = scoped.find((k) => k.name === name);
377
+ if (!ks) {
378
+ throw new UserError(
379
+ `Knowledge source '${name}' is not configured on this server; configured: ${scoped.map((k) => k.name).join(", ")}`
380
+ );
381
+ }
382
+ return ks;
383
+ }
384
+ function filterKs(names) {
385
+ if (!names || names.length === 0) return scoped;
386
+ const out = [];
387
+ for (const name of names) {
388
+ const ks = scoped.find((k) => k.name === name);
389
+ if (!ks) {
390
+ throw new UserError(
391
+ `Knowledge source '${name}' is not configured on this server; configured: ${scoped.map((k) => k.name).join(", ")}`
392
+ );
393
+ }
394
+ out.push(ks);
395
+ }
396
+ return out;
397
+ }
398
+ server.addTool({
399
+ name: "list_ks",
400
+ description: "List the knowledge sources served by this server, with entry counts",
401
+ parameters: z.object({}),
402
+ annotations: { readOnlyHint: true },
403
+ execute: async () => {
404
+ const out = [];
405
+ for (const ks of scoped) {
406
+ out.push({
407
+ name: ks.name,
408
+ description: ks.description,
409
+ entries: await countEntries(ks),
410
+ embedding_model: ks.embedding_model,
411
+ embedding_dim: ks.embedding_dim
412
+ });
413
+ }
414
+ return JSON.stringify(out, null, 2);
415
+ }
416
+ });
417
+ server.addTool({
418
+ name: "search",
419
+ description: "Hybrid semantic + keyword search across knowledge sources (pgvector cosine similarity fused with tsvector full-text via reciprocal rank fusion). Returns ranked chunks with snippets and scores.",
420
+ parameters: z.object({
421
+ query: z.string().describe("Natural language search query"),
422
+ ks: z.array(z.string()).optional().describe("Restrict search to these knowledge source names (default: all configured)"),
423
+ mode: z.enum(["hybrid", "vector", "keyword"]).default("hybrid").describe("hybrid = vector+keyword fusion, vector = semantic only, keyword = full-text only"),
424
+ limit: z.number().int().min(1).max(50).default(8).describe("Max results"),
425
+ tags: z.array(z.string()).optional().describe("Filter entries by tags (any-of match)")
426
+ }),
427
+ annotations: { readOnlyHint: true },
428
+ execute: async (args) => {
429
+ const hits = await search(filterKs(args.ks), {
430
+ query: args.query,
431
+ mode: args.mode,
432
+ limit: args.limit,
433
+ tags: args.tags
434
+ });
435
+ if (hits.length === 0) return "No results found.";
436
+ return hits.map(
437
+ (h, i) => `${i + 1}. [${h.ks}] id=${h.id} score=${h.score.toFixed(4)} (${h.source}#chunk${h.chunk_index})
438
+ title: ${h.title}
439
+ ${h.snippet.replace(/\s+/g, " ").trim()}`
440
+ ).join("\n\n");
441
+ }
442
+ });
443
+ server.addTool({
444
+ name: "get_entry",
445
+ description: "Fetch a single knowledge entry (chunk) by id, including full content and metadata",
446
+ parameters: z.object({
447
+ ks: ksRef,
448
+ id: z.number().int().describe("Entry id as returned by search or list_entries")
449
+ }),
450
+ annotations: { readOnlyHint: true },
451
+ execute: async (args) => {
452
+ const entry = await getEntry(resolveKs(args.ks), args.id);
453
+ if (!entry) throw new UserError(`Entry ${args.id} not found`);
454
+ return JSON.stringify(
455
+ {
456
+ id: Number(entry.id),
457
+ source: entry.source,
458
+ title: entry.title,
459
+ chunk_index: entry.chunk_index,
460
+ content: entry.content,
461
+ metadata: entry.metadata
462
+ },
463
+ null,
464
+ 2
465
+ );
466
+ }
467
+ });
468
+ server.addTool({
469
+ name: "list_entries",
470
+ description: "List entries of a knowledge source (id, source, title, chunk index, size)",
471
+ parameters: z.object({
472
+ ks: ksRef,
473
+ source: z.string().optional().describe("Filter by source (e.g. a file path)"),
474
+ limit: z.number().int().min(1).max(200).default(20),
475
+ offset: z.number().int().min(0).default(0)
476
+ }),
477
+ annotations: { readOnlyHint: true },
478
+ execute: async (args) => {
479
+ const rows = await listEntries(resolveKs(args.ks), {
480
+ source: args.source,
481
+ limit: args.limit,
482
+ offset: args.offset
483
+ });
484
+ return JSON.stringify(rows, null, 2);
485
+ }
486
+ });
487
+ server.addTool({
488
+ name: "list_sources",
489
+ description: "List ingested documents/sources (file paths or manual source names) with chunk counts",
490
+ parameters: z.object({
491
+ ks: ksRef
492
+ }),
493
+ annotations: { readOnlyHint: true },
494
+ execute: async (args) => {
495
+ const ks = args.ks ? [resolveKs(args.ks)] : scoped;
496
+ return JSON.stringify(await listSources(ks), null, 2);
497
+ }
498
+ });
499
+ process.on("SIGINT", () => void closePool().finally(() => process.exit(0)));
500
+ process.on("SIGTERM", () => void closePool().finally(() => process.exit(0)));
501
+ if (transport === "http") {
502
+ await server.start({
503
+ transportType: "httpStream",
504
+ httpStream: { port }
505
+ });
506
+ console.error(`gresmcp MCP server listening on http://localhost:${port}/mcp (ks: ${scoped.map((k) => k.name).join(", ")})`);
507
+ } else {
508
+ await server.start({ transportType: "stdio" });
509
+ }
510
+ }
511
+ main().catch((err) => {
512
+ console.error(`gresmcp: ${err instanceof Error ? err.message : err}`);
513
+ void closePool().finally(() => process.exit(1));
514
+ });
515
+ //# sourceMappingURL=mcp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mcp.ts","../src/db.ts","../src/config.ts","../src/search.ts","../src/ollama.ts","../src/version.ts"],"sourcesContent":["import { FastMCP, UserError } from \"fastmcp\";\r\nimport { z } from \"zod\";\r\nimport {\r\n initDb,\r\n closePool,\r\n listKs,\r\n countEntries,\r\n type KnowledgeSource,\r\n} from \"./db.js\";\r\nimport {\r\n search,\r\n getEntry,\r\n listEntries,\r\n listSources,\r\n type SearchMode,\r\n} from \"./search.js\";\r\nimport { VERSION } from \"./version.js\";\r\n\r\nfunction parseArgs(argv: string[]): {\r\n ksNames: string[];\r\n all: boolean;\r\n transport: \"stdio\" | \"http\";\r\n port: number;\r\n} {\r\n const ksNames: string[] = [];\r\n let all = false;\r\n let transport: \"stdio\" | \"http\" = \"stdio\";\r\n let port = 8080;\r\n for (let i = 0; i < argv.length; i++) {\r\n const arg = argv[i]!;\r\n if (arg === \"--all\") {\r\n all = true;\r\n } else if (arg === \"--transport\" || arg === \"-t\") {\r\n const value = argv[++i];\r\n if (value !== \"stdio\" && value !== \"http\") {\r\n throw new Error(`invalid --transport value '${value}', expected 'stdio' or 'http'`);\r\n }\r\n transport = value;\r\n } else if (arg === \"--port\" || arg === \"-p\") {\r\n port = parseInt(argv[++i] ?? \"\", 10);\r\n if (!Number.isFinite(port) || port <= 0) throw new Error(`invalid --port value`);\r\n } else if (arg === \"--help\" || arg === \"-h\") {\r\n console.error(`usage: mcp [ks1,ks2,... | --all] [--transport stdio|http] [--port N]`);\r\n process.exit(0);\r\n } else if (arg.startsWith(\"-\")) {\r\n throw new Error(`unknown option '${arg}'`);\r\n } else {\r\n ksNames.push(...arg.split(\",\").map((s) => s.trim()).filter(Boolean));\r\n }\r\n }\r\n if (!all && ksNames.length === 0) {\r\n const env = process.env.GRESMCP_KS;\r\n if (env) ksNames.push(...env.split(\",\").map((s) => s.trim()).filter(Boolean));\r\n }\r\n return { ksNames, all, transport, port };\r\n}\r\n\r\nasync function main(): Promise<void> {\r\n const { ksNames, all, transport, port } = parseArgs(process.argv.slice(2));\r\n await initDb();\r\n\r\n const available = await listKs();\r\n let scoped: KnowledgeSource[];\r\n if (all) {\r\n scoped = available;\r\n } else {\r\n const wanted = new Set(ksNames);\r\n const missing = ksNames.filter((n) => !available.some((k) => k.name === n));\r\n if (missing.length > 0) {\r\n throw new Error(\r\n `unknown knowledge source(s): ${missing.join(\", \")}. Available: ${available.map((k) => k.name).join(\", \") || \"(none)\"}`,\r\n );\r\n }\r\n scoped = available.filter((k) => wanted.has(k.name));\r\n }\r\n if (scoped.length === 0) {\r\n throw new Error(\"no knowledge sources configured; pass ks names as arguments or use --all\");\r\n }\r\n\r\n const descriptions = scoped\r\n .map((k) => `- ${k.name}: ${k.description || \"(no description)\"} [model=${k.embedding_model}, dim=${k.embedding_dim}]`)\r\n .join(\"\\n\");\r\n\r\n const server = new FastMCP({\r\n name: \"gresmcp\",\r\n version: VERSION,\r\n instructions:\r\n `This server exposes read-only access to Postgres-backed knowledge sources (RAG: pgvector + tsvector hybrid search).\\n` +\r\n `Available knowledge sources:\\n${descriptions}\\n` +\r\n `Use 'search' with hybrid mode for best results, 'get_entry' to read a full chunk, 'list_sources' to see what documents exist.`,\r\n });\r\n\r\n const ksRef = z\r\n .string()\r\n .optional()\r\n .describe(\"Knowledge source name; optional when the server has exactly one\");\r\n\r\n function resolveKs(name?: string): KnowledgeSource {\r\n if (!name) {\r\n if (scoped.length === 1) return scoped[0]!;\r\n throw new UserError(\r\n `Multiple knowledge sources configured (${scoped.map((k) => k.name).join(\", \")}); specify one with the 'ks' parameter`,\r\n );\r\n }\r\n const ks = scoped.find((k) => k.name === name);\r\n if (!ks) {\r\n throw new UserError(\r\n `Knowledge source '${name}' is not configured on this server; configured: ${scoped.map((k) => k.name).join(\", \")}`,\r\n );\r\n }\r\n return ks;\r\n }\r\n\r\n function filterKs(names?: string[]): KnowledgeSource[] {\r\n if (!names || names.length === 0) return scoped;\r\n const out: KnowledgeSource[] = [];\r\n for (const name of names) {\r\n const ks = scoped.find((k) => k.name === name);\r\n if (!ks) {\r\n throw new UserError(\r\n `Knowledge source '${name}' is not configured on this server; configured: ${scoped.map((k) => k.name).join(\", \")}`,\r\n );\r\n }\r\n out.push(ks);\r\n }\r\n return out;\r\n }\r\n\r\n server.addTool({\r\n name: \"list_ks\",\r\n description: \"List the knowledge sources served by this server, with entry counts\",\r\n parameters: z.object({}),\r\n annotations: { readOnlyHint: true },\r\n execute: async () => {\r\n const out = [];\r\n for (const ks of scoped) {\r\n out.push({\r\n name: ks.name,\r\n description: ks.description,\r\n entries: await countEntries(ks),\r\n embedding_model: ks.embedding_model,\r\n embedding_dim: ks.embedding_dim,\r\n });\r\n }\r\n return JSON.stringify(out, null, 2);\r\n },\r\n });\r\n\r\n server.addTool({\r\n name: \"search\",\r\n description:\r\n \"Hybrid semantic + keyword search across knowledge sources (pgvector cosine similarity fused with tsvector full-text via reciprocal rank fusion). Returns ranked chunks with snippets and scores.\",\r\n parameters: z.object({\r\n query: z.string().describe(\"Natural language search query\"),\r\n ks: z.array(z.string()).optional().describe(\"Restrict search to these knowledge source names (default: all configured)\"),\r\n mode: z.enum([\"hybrid\", \"vector\", \"keyword\"]).default(\"hybrid\").describe(\"hybrid = vector+keyword fusion, vector = semantic only, keyword = full-text only\"),\r\n limit: z.number().int().min(1).max(50).default(8).describe(\"Max results\"),\r\n tags: z.array(z.string()).optional().describe(\"Filter entries by tags (any-of match)\"),\r\n }),\r\n annotations: { readOnlyHint: true },\r\n execute: async (args) => {\r\n const hits = await search(filterKs(args.ks), {\r\n query: args.query,\r\n mode: args.mode as SearchMode,\r\n limit: args.limit,\r\n tags: args.tags,\r\n });\r\n if (hits.length === 0) return \"No results found.\";\r\n return hits\r\n .map(\r\n (h, i) =>\r\n `${i + 1}. [${h.ks}] id=${h.id} score=${h.score.toFixed(4)} (${h.source}#chunk${h.chunk_index})\\n title: ${h.title}\\n ${h.snippet.replace(/\\s+/g, \" \").trim()}`,\r\n )\r\n .join(\"\\n\\n\");\r\n },\r\n });\r\n\r\n server.addTool({\r\n name: \"get_entry\",\r\n description: \"Fetch a single knowledge entry (chunk) by id, including full content and metadata\",\r\n parameters: z.object({\r\n ks: ksRef,\r\n id: z.number().int().describe(\"Entry id as returned by search or list_entries\"),\r\n }),\r\n annotations: { readOnlyHint: true },\r\n execute: async (args) => {\r\n const entry = await getEntry(resolveKs(args.ks), args.id);\r\n if (!entry) throw new UserError(`Entry ${args.id} not found`);\r\n return JSON.stringify(\r\n {\r\n id: Number(entry.id),\r\n source: entry.source,\r\n title: entry.title,\r\n chunk_index: entry.chunk_index,\r\n content: entry.content,\r\n metadata: entry.metadata,\r\n },\r\n null,\r\n 2,\r\n );\r\n },\r\n });\r\n\r\n server.addTool({\r\n name: \"list_entries\",\r\n description: \"List entries of a knowledge source (id, source, title, chunk index, size)\",\r\n parameters: z.object({\r\n ks: ksRef,\r\n source: z.string().optional().describe(\"Filter by source (e.g. a file path)\"),\r\n limit: z.number().int().min(1).max(200).default(20),\r\n offset: z.number().int().min(0).default(0),\r\n }),\r\n annotations: { readOnlyHint: true },\r\n execute: async (args) => {\r\n const rows = await listEntries(resolveKs(args.ks), {\r\n source: args.source,\r\n limit: args.limit,\r\n offset: args.offset,\r\n });\r\n return JSON.stringify(rows, null, 2);\r\n },\r\n });\r\n\r\n server.addTool({\r\n name: \"list_sources\",\r\n description: \"List ingested documents/sources (file paths or manual source names) with chunk counts\",\r\n parameters: z.object({\r\n ks: ksRef,\r\n }),\r\n annotations: { readOnlyHint: true },\r\n execute: async (args) => {\r\n const ks = args.ks ? [resolveKs(args.ks)] : scoped;\r\n return JSON.stringify(await listSources(ks), null, 2);\r\n },\r\n });\r\n\r\n process.on(\"SIGINT\", () => void closePool().finally(() => process.exit(0)));\r\n process.on(\"SIGTERM\", () => void closePool().finally(() => process.exit(0)));\r\n\r\n if (transport === \"http\") {\r\n await server.start({\r\n transportType: \"httpStream\",\r\n httpStream: { port },\r\n });\r\n console.error(`gresmcp MCP server listening on http://localhost:${port}/mcp (ks: ${scoped.map((k) => k.name).join(\", \")})`);\r\n } else {\r\n await server.start({ transportType: \"stdio\" });\r\n }\r\n}\r\n\r\nmain().catch((err) => {\r\n console.error(`gresmcp: ${err instanceof Error ? err.message : err}`);\r\n void closePool().finally(() => process.exit(1));\r\n});\r\n","import { Pool } from \"pg\";\r\nimport { toSql } from \"pgvector\";\r\nimport { createHash } from \"node:crypto\";\r\nimport { databaseUrl, DEFAULT_OLLAMA_URL } from \"./config.js\";\r\n\r\nlet pool: Pool | undefined;\r\n\r\nexport function getPool(): Pool {\r\n if (!pool) {\r\n pool = new Pool({ connectionString: databaseUrl(), max: 5 });\r\n }\r\n return pool;\r\n}\r\n\r\nexport async function closePool(): Promise<void> {\r\n if (pool) {\r\n await pool.end();\r\n pool = undefined;\r\n }\r\n}\r\n\r\nexport interface KnowledgeSource {\r\n id: string;\r\n name: string;\r\n description: string;\r\n embedding_model: string;\r\n embedding_dim: number;\r\n ollama_url: string;\r\n created_at: Date;\r\n updated_at: Date;\r\n}\r\n\r\nexport interface EntryRow {\r\n id: string | number;\r\n source: string;\r\n title: string;\r\n chunk_index: number;\r\n content: string;\r\n content_hash: string;\r\n metadata: Record<string, unknown>;\r\n created_at: Date;\r\n}\r\n\r\nexport function entryTableName(ks: Pick<KnowledgeSource, \"id\">): string {\r\n const name = `entry_${ks.id.replace(/-/g, \"\")}`;\r\n assertTableName(name);\r\n return name;\r\n}\r\n\r\nexport function assertTableName(name: string): void {\r\n if (!/^entry_[0-9a-f]{32}$/.test(name)) {\r\n throw new Error(`Refusing to use unsafe table name: ${name}`);\r\n }\r\n}\r\n\r\nexport function contentHash(content: string): string {\r\n return createHash(\"sha256\").update(content).digest(\"hex\");\r\n}\r\n\r\nexport async function initDb(): Promise<void> {\r\n const p = getPool();\r\n await p.query(`CREATE EXTENSION IF NOT EXISTS vector`);\r\n await p.query(`\r\n CREATE TABLE IF NOT EXISTS ks (\r\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\r\n name text NOT NULL UNIQUE,\r\n description text NOT NULL DEFAULT '',\r\n embedding_model text NOT NULL,\r\n embedding_dim integer NOT NULL,\r\n ollama_url text NOT NULL DEFAULT '${DEFAULT_OLLAMA_URL}',\r\n created_at timestamptz NOT NULL DEFAULT now(),\r\n updated_at timestamptz NOT NULL DEFAULT now()\r\n )\r\n `);\r\n}\r\n\r\nexport async function createKs(input: {\r\n name: string;\r\n description?: string;\r\n model: string;\r\n dim: number;\r\n url: string;\r\n}): Promise<KnowledgeSource> {\r\n const res = await getPool().query(\r\n `INSERT INTO ks (name, description, embedding_model, embedding_dim, ollama_url)\r\n VALUES ($1, $2, $3, $4, $5)\r\n RETURNING *`,\r\n [input.name, input.description ?? \"\", input.model, input.dim, input.url],\r\n );\r\n const ks = res.rows[0] as KnowledgeSource;\r\n await ensureEntryTable(ks);\r\n return ks;\r\n}\r\n\r\nexport async function listKs(): Promise<KnowledgeSource[]> {\r\n const res = await getPool().query(`SELECT * FROM ks ORDER BY name`);\r\n return res.rows as KnowledgeSource[];\r\n}\r\n\r\nexport async function getKs(name: string): Promise<KnowledgeSource | undefined> {\r\n const res = await getPool().query(`SELECT * FROM ks WHERE name = $1`, [name]);\r\n return res.rows[0] as KnowledgeSource | undefined;\r\n}\r\n\r\nexport async function updateKs(\r\n id: string,\r\n fields: { name?: string; description?: string; ollama_url?: string },\r\n): Promise<KnowledgeSource> {\r\n const sets: string[] = [];\r\n const params: unknown[] = [];\r\n if (fields.name !== undefined) {\r\n params.push(fields.name);\r\n sets.push(`name = $${params.length}`);\r\n }\r\n if (fields.description !== undefined) {\r\n params.push(fields.description);\r\n sets.push(`description = $${params.length}`);\r\n }\r\n if (fields.ollama_url !== undefined) {\r\n params.push(fields.ollama_url);\r\n sets.push(`ollama_url = $${params.length}`);\r\n }\r\n if (sets.length === 0) {\r\n const res = await getPool().query(`SELECT * FROM ks WHERE id = $1`, [id]);\r\n return res.rows[0] as KnowledgeSource;\r\n }\r\n sets.push(`updated_at = now()`);\r\n params.push(id);\r\n const res = await getPool().query(\r\n `UPDATE ks SET ${sets.join(\", \")} WHERE id = $${params.length} RETURNING *`,\r\n params,\r\n );\r\n return res.rows[0] as KnowledgeSource;\r\n}\r\n\r\nexport async function deleteKs(ks: KnowledgeSource): Promise<void> {\r\n const tbl = entryTableName(ks);\r\n await getPool().query(`DROP TABLE IF EXISTS ${tbl}`);\r\n await getPool().query(`DELETE FROM ks WHERE id = $1`, [ks.id]);\r\n}\r\n\r\nexport async function ensureEntryTable(ks: KnowledgeSource): Promise<void> {\r\n const tbl = entryTableName(ks);\r\n const p = getPool();\r\n await p.query(`\r\n CREATE TABLE IF NOT EXISTS ${tbl} (\r\n id bigserial PRIMARY KEY,\r\n source text NOT NULL,\r\n title text NOT NULL DEFAULT '',\r\n chunk_index integer NOT NULL DEFAULT 0,\r\n content text NOT NULL,\r\n content_hash text NOT NULL,\r\n metadata jsonb NOT NULL DEFAULT '{}',\r\n tsv tsvector GENERATED ALWAYS AS (to_tsvector('english', content)) STORED,\r\n embedding vector(${ks.embedding_dim}) NOT NULL,\r\n created_at timestamptz NOT NULL DEFAULT now()\r\n )\r\n `);\r\n await p.query(`CREATE INDEX IF NOT EXISTS ${tbl}_tsv_idx ON ${tbl} USING gin (tsv)`);\r\n await p.query(`CREATE INDEX IF NOT EXISTS ${tbl}_emb_idx ON ${tbl} USING hnsw (embedding vector_cosine_ops)`);\r\n await p.query(`CREATE INDEX IF NOT EXISTS ${tbl}_source_idx ON ${tbl} (source)`);\r\n}\r\n\r\nexport interface ChunkInsert {\r\n source: string;\r\n title: string;\r\n chunk_index: number;\r\n content: string;\r\n metadata: Record<string, unknown>;\r\n embedding: number[];\r\n}\r\n\r\nexport async function insertChunks(ks: KnowledgeSource, rows: ChunkInsert[]): Promise<number> {\r\n if (rows.length === 0) return 0;\r\n const tbl = entryTableName(ks);\r\n const p = getPool();\r\n const BATCH = 25;\r\n let inserted = 0;\r\n for (let i = 0; i < rows.length; i += BATCH) {\r\n const batch = rows.slice(i, i + BATCH);\r\n const values: string[] = [];\r\n const params: unknown[] = [];\r\n batch.forEach((r, j) => {\r\n const b = j * 7;\r\n values.push(`($${b + 1}, $${b + 2}, $${b + 3}, $${b + 4}, $${b + 5}, $${b + 6}, $${b + 7}::vector)`);\r\n params.push(r.source, r.title, r.chunk_index, r.content, contentHash(r.content), JSON.stringify(r.metadata), toSql(r.embedding));\r\n });\r\n const res = await p.query(\r\n `INSERT INTO ${tbl} (source, title, chunk_index, content, content_hash, metadata, embedding)\r\n VALUES ${values.join(\", \")}`,\r\n params,\r\n );\r\n inserted += res.rowCount ?? 0;\r\n }\r\n return inserted;\r\n}\r\n\r\nexport async function deleteBySource(ks: KnowledgeSource, source: string): Promise<number> {\r\n const tbl = entryTableName(ks);\r\n const res = await getPool().query(`DELETE FROM ${tbl} WHERE source = $1`, [source]);\r\n return res.rowCount ?? 0;\r\n}\r\n\r\nexport async function sourceChunkHashes(\r\n ks: KnowledgeSource,\r\n source: string,\r\n): Promise<Map<number, string>> {\r\n const tbl = entryTableName(ks);\r\n const res = await getPool().query(\r\n `SELECT chunk_index, content_hash FROM ${tbl} WHERE source = $1`,\r\n [source],\r\n );\r\n const map = new Map<number, string>();\r\n for (const row of res.rows) {\r\n map.set(Number(row.chunk_index), String(row.content_hash));\r\n }\r\n return map;\r\n}\r\n\r\nexport async function countEntries(ks: KnowledgeSource): Promise<number> {\r\n const tbl = entryTableName(ks);\r\n const res = await getPool().query(`SELECT count(*)::int AS n FROM ${tbl}`);\r\n return Number(res.rows[0]?.n ?? 0);\r\n}\r\n","export const DEFAULT_DATABASE_URL = \"postgresql://gres:gres@localhost/gres\";\r\nexport const DEFAULT_OLLAMA_URL = \"http://localhost:11434\";\r\n\r\nexport function databaseUrl(): string {\r\n return process.env.GRESMCP_DATABASE_URL || DEFAULT_DATABASE_URL;\r\n}\r\n\r\nexport function normalizeUrl(url: string): string {\r\n return url.replace(/\\/+$/, \"\");\r\n}\r\n\r\nexport function ollamaUrl(override?: string): string {\r\n const raw = override || process.env.GRESMCP_OLLAMA_URL || DEFAULT_OLLAMA_URL;\r\n return normalizeUrl(raw);\r\n}\r\n","import { toSql } from \"pgvector\";\r\nimport { getPool, entryTableName, type EntryRow, type KnowledgeSource } from \"./db.js\";\r\nimport { embed } from \"./ollama.js\";\r\nimport { normalizeUrl } from \"./config.js\";\r\n\r\nexport type SearchMode = \"hybrid\" | \"vector\" | \"keyword\";\r\n\r\nexport interface SearchHit {\r\n ks: string;\r\n id: number;\r\n source: string;\r\n title: string;\r\n chunk_index: number;\r\n score: number;\r\n snippet: string;\r\n metadata: Record<string, unknown>;\r\n}\r\n\r\nconst RRF_K = 60;\r\nconst HEADLINE_OPTS = \"MaxWords=35, MinWords=15, ShortWord=0, MaxFragments=1, StartSel=[, StopSel=]\";\r\n\r\ninterface SqlParts {\r\n sql: string;\r\n params: unknown[];\r\n}\r\n\r\nfunction tagClause(params: unknown[], tags?: string[]): string {\r\n if (!tags || tags.length === 0) return \"\";\r\n params.push(tags);\r\n return ` AND metadata->'tags' ?| $${params.length}::text[]`;\r\n}\r\n\r\nfunction buildSearchSql(\r\n tbl: string,\r\n mode: SearchMode,\r\n limit: number,\r\n tags?: string[],\r\n): SqlParts {\r\n const params: unknown[] = [];\r\n if (mode === \"vector\") params.push(\"__VECTOR__\");\r\n else if (mode === \"keyword\") params.push(\"__QUERY__\");\r\n else params.push(\"__VECTOR__\", \"__QUERY__\");\r\n const tag = tagClause(params, tags);\r\n\r\n if (mode === \"vector\") {\r\n const sql = `\r\n SELECT id, source, title, chunk_index, metadata,\r\n 1 - (embedding <=> $1::vector) AS score,\r\n left(regexp_replace(content, '\\\\s+', ' ', 'g'), 320) AS snippet\r\n FROM ${tbl}\r\n WHERE true${tag}\r\n ORDER BY embedding <=> $1::vector\r\n LIMIT ${limit}`;\r\n return { sql, params };\r\n }\r\n\r\n if (mode === \"keyword\") {\r\n const sql = `\r\n WITH q AS (SELECT websearch_to_tsquery('english', $1) AS query)\r\n SELECT e.id, e.source, e.title, e.chunk_index, e.metadata,\r\n ts_rank_cd(e.tsv, q.query) AS score,\r\n ts_headline('english', e.content, q.query, '${HEADLINE_OPTS}') AS snippet\r\n FROM ${tbl} e CROSS JOIN q\r\n WHERE e.tsv @@ q.query${tag}\r\n ORDER BY score DESC, e.id\r\n LIMIT ${limit}`;\r\n return { sql, params };\r\n }\r\n\r\n const inner = Math.min(Math.max(limit * 4, 20), 100);\r\n const sql = `\r\n WITH v AS (\r\n SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <=> $1::vector) AS r\r\n FROM ${tbl}\r\n WHERE true${tag}\r\n ORDER BY embedding <=> $1::vector\r\n LIMIT ${inner}\r\n ),\r\n k AS (\r\n SELECT id, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(tsv, q.query) DESC) AS r\r\n FROM ${tbl} CROSS JOIN (SELECT websearch_to_tsquery('english', $2) AS query) q\r\n WHERE tsv @@ q.query${tag}\r\n ORDER BY ts_rank_cd(tsv, q.query) DESC\r\n LIMIT ${inner}\r\n ),\r\n s AS (\r\n SELECT COALESCE(v.id, k.id) AS id,\r\n COALESCE(1.0 / (${RRF_K} + v.r), 0) + COALESCE(1.0 / (${RRF_K} + k.r), 0) AS score,\r\n (v.r IS NOT NULL) AS vhit,\r\n (k.r IS NOT NULL) AS khit\r\n FROM v FULL JOIN k ON k.id = v.id\r\n )\r\n SELECT e.id, e.source, e.title, e.chunk_index, e.metadata, s.score,\r\n CASE WHEN s.khit\r\n THEN ts_headline('english', e.content, websearch_to_tsquery('english', $2), '${HEADLINE_OPTS}')\r\n ELSE left(regexp_replace(e.content, '\\\\s+', ' ', 'g'), 320)\r\n END AS snippet\r\n FROM s\r\n JOIN ${tbl} e ON e.id = s.id\r\n ORDER BY s.score DESC, e.id\r\n LIMIT ${limit}`;\r\n return { sql, params };\r\n}\r\n\r\nasync function queryEmbedding(ks: KnowledgeSource, query: string, cache: Map<string, number[]>): Promise<number[]> {\r\n const key = `${normalizeUrl(ks.ollama_url)}::${ks.embedding_model}::${query}`;\r\n const cached = cache.get(key);\r\n if (cached) return cached;\r\n const embs = await embed(ks.ollama_url, ks.embedding_model, [query], 30_000);\r\n const emb = embs[0];\r\n if (!emb) {\r\n throw new Error(`Ollama returned an empty embedding for query on ks '${ks.name}'`);\r\n }\r\n cache.set(key, emb);\r\n return emb;\r\n}\r\n\r\nexport interface SearchArgs {\r\n query: string;\r\n mode?: SearchMode;\r\n limit?: number;\r\n tags?: string[];\r\n ksFilter?: string[];\r\n}\r\n\r\nexport async function search(ksList: KnowledgeSource[], args: SearchArgs): Promise<SearchHit[]> {\r\n const mode = args.mode ?? \"hybrid\";\r\n const limit = Math.min(Math.max(args.limit ?? 8, 1), 50);\r\n let targets = ksList;\r\n if (args.ksFilter && args.ksFilter.length > 0) {\r\n const wanted = new Set(args.ksFilter);\r\n targets = ksList.filter((k) => wanted.has(k.name));\r\n }\r\n if (targets.length === 0) return [];\r\n\r\n const cache = new Map<string, number[]>();\r\n const all: SearchHit[] = [];\r\n for (const ks of targets) {\r\n const tbl = entryTableName(ks);\r\n const { sql, params } = buildSearchSql(tbl, mode, limit, args.tags);\r\n const resolved: unknown[] = [];\r\n for (const p of params) {\r\n if (p === \"__VECTOR__\") {\r\n const emb = await queryEmbedding(ks, args.query, cache);\r\n if (emb.length !== ks.embedding_dim) {\r\n throw new Error(\r\n `Embedding dimension mismatch for ks '${ks.name}': expected ${ks.embedding_dim}, got ${emb.length}`,\r\n );\r\n }\r\n resolved.push(toSql(emb));\r\n } else if (p === \"__QUERY__\") {\r\n resolved.push(args.query);\r\n } else {\r\n resolved.push(p);\r\n }\r\n }\r\n const res = await getPool().query(sql, resolved);\r\n for (const row of res.rows) {\r\n all.push({\r\n ks: ks.name,\r\n id: Number(row.id),\r\n source: String(row.source),\r\n title: String(row.title ?? \"\"),\r\n chunk_index: Number(row.chunk_index ?? 0),\r\n score: Number(row.score ?? 0),\r\n snippet: String(row.snippet ?? \"\"),\r\n metadata: (row.metadata ?? {}) as Record<string, unknown>,\r\n });\r\n }\r\n }\r\n all.sort((a, b) => b.score - a.score || a.ks.localeCompare(b.ks));\r\n return all.slice(0, limit);\r\n}\r\n\r\nexport async function getEntry(ks: KnowledgeSource, id: number): Promise<EntryRow | undefined> {\r\n const tbl = entryTableName(ks);\r\n const res = await getPool().query(\r\n `SELECT id, source, title, chunk_index, content, content_hash, metadata, created_at\r\n FROM ${tbl} WHERE id = $1`,\r\n [id],\r\n );\r\n return res.rows[0] as EntryRow | undefined;\r\n}\r\n\r\nexport interface EntryListItem {\r\n id: number;\r\n source: string;\r\n title: string;\r\n chunk_index: number;\r\n chars: number;\r\n created_at: Date;\r\n}\r\n\r\nexport async function listEntries(\r\n ks: KnowledgeSource,\r\n opts: { source?: string; limit?: number; offset?: number } = {},\r\n): Promise<EntryListItem[]> {\r\n const tbl = entryTableName(ks);\r\n const limit = Math.min(Math.max(opts.limit ?? 20, 1), 200);\r\n const offset = Math.max(opts.offset ?? 0, 0);\r\n const params: unknown[] = [];\r\n let where = \"\";\r\n if (opts.source) {\r\n params.push(opts.source);\r\n where = `WHERE source = $${params.length}`;\r\n }\r\n params.push(limit, offset);\r\n const res = await getPool().query(\r\n `SELECT id, source, title, chunk_index, length(content) AS chars, created_at\r\n FROM ${tbl} ${where}\r\n ORDER BY source, chunk_index, id\r\n LIMIT $${params.length - 1} OFFSET $${params.length}`,\r\n params,\r\n );\r\n return res.rows.map((r) => ({\r\n id: Number(r.id),\r\n source: String(r.source),\r\n title: String(r.title ?? \"\"),\r\n chunk_index: Number(r.chunk_index),\r\n chars: Number(r.chars),\r\n created_at: r.created_at as Date,\r\n }));\r\n}\r\n\r\nexport interface SourceInfo {\r\n ks: string;\r\n source: string;\r\n title: string;\r\n chunks: number;\r\n chars: number;\r\n}\r\n\r\nexport async function listSources(ksList: KnowledgeSource[]): Promise<SourceInfo[]> {\r\n const out: SourceInfo[] = [];\r\n for (const ks of ksList) {\r\n const tbl = entryTableName(ks);\r\n const res = await getPool().query(\r\n `SELECT source, max(title) AS title, count(*)::int AS chunks, sum(length(content))::bigint AS chars\r\n FROM ${tbl}\r\n GROUP BY source\r\n ORDER BY source`,\r\n );\r\n for (const r of res.rows) {\r\n out.push({\r\n ks: ks.name,\r\n source: String(r.source),\r\n title: String(r.title ?? \"\"),\r\n chunks: Number(r.chunks),\r\n chars: Number(r.chars),\r\n });\r\n }\r\n }\r\n return out;\r\n}\r\n","import { normalizeUrl } from \"./config.js\";\r\n\r\nexport class OllamaError extends Error {\r\n constructor(\r\n message: string,\r\n public readonly status?: number,\r\n ) {\r\n super(message);\r\n this.name = \"OllamaError\";\r\n }\r\n}\r\n\r\nexport async function embed(\r\n baseUrl: string,\r\n model: string,\r\n inputs: string[],\r\n timeoutMs = 120_000,\r\n): Promise<number[][]> {\r\n const url = `${normalizeUrl(baseUrl)}/api/embed`;\r\n let res: Response;\r\n try {\r\n res = await fetch(url, {\r\n method: \"POST\",\r\n headers: { \"content-type\": \"application/json\" },\r\n body: JSON.stringify({ model, input: inputs }),\r\n signal: AbortSignal.timeout(timeoutMs),\r\n });\r\n } catch (err) {\r\n throw new OllamaError(\r\n `Cannot reach Ollama at ${normalizeUrl(baseUrl)} (${err instanceof Error ? err.message : err})`,\r\n );\r\n }\r\n if (!res.ok) {\r\n const body = await res.text().catch(() => \"\");\r\n throw new OllamaError(`Ollama returned ${res.status} for ${url}: ${body.slice(0, 300)}`, res.status);\r\n }\r\n const data = (await res.json()) as { embeddings?: number[][]; error?: string };\r\n if (data.error) {\r\n throw new OllamaError(`Ollama error: ${data.error}`);\r\n }\r\n if (!Array.isArray(data.embeddings) || data.embeddings.length !== inputs.length) {\r\n throw new OllamaError(`Unexpected response shape from ${url}`);\r\n }\r\n return data.embeddings;\r\n}\r\n\r\nexport async function embedBatch(\r\n baseUrl: string,\r\n model: string,\r\n inputs: string[],\r\n batchSize = 32,\r\n onProgress?: (done: number, total: number) => void,\r\n): Promise<number[][]> {\r\n const out: number[][] = [];\r\n let done = 0;\r\n for (let i = 0; i < inputs.length; i += batchSize) {\r\n const batch = inputs.slice(i, i + batchSize);\r\n const embs = await embed(baseUrl, model, batch);\r\n out.push(...embs);\r\n done += batch.length;\r\n onProgress?.(done, inputs.length);\r\n }\r\n return out;\r\n}\r\n\r\nexport async function probeModel(\r\n baseUrl: string,\r\n model: string,\r\n): Promise<{ dim: number }> {\r\n const embs = await embed(baseUrl, model, [\"gresmcp dimension probe\"], 30_000);\r\n const first = embs[0];\r\n if (!first || first.length === 0) {\r\n throw new OllamaError(`Ollama returned an empty embedding for model ${model}`);\r\n }\r\n return { dim: first.length };\r\n}\r\n","export const VERSION = \"1.0.0\";\r\n"],"mappings":"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","names":["toSql","sql","toSql"]}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@gresmcp/mcp",
3
+ "version": "1.0.0",
4
+ "description": "From raw docs to refined answers — Gresy fires your documents into solid Postgres knowledge, entirely local, your MCP.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://gitea.com/fairking/gresmcp.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://gitea.com/fairking/gresmcp/issues"
13
+ },
14
+ "homepage": "https://gitea.com/fairking/gresmcp#readme",
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
18
+ "bin": {
19
+ "mcp": "dist/mcp.js",
20
+ "gresmcp": "dist/cli.js"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "typecheck": "tsc --noEmit",
29
+ "test": "vitest run",
30
+ "test:watch": "vitest",
31
+ "mcp": "node dist/mcp.js",
32
+ "gresmcp": "node dist/cli.js",
33
+ "smoke": "node scripts/smoke-mcp.mjs",
34
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
35
+ },
36
+ "dependencies": {
37
+ "commander": "^14.0.0",
38
+ "fastmcp": "^4.20.3",
39
+ "jsdom": "^26.1.0",
40
+ "pg": "^8.16.0",
41
+ "pgvector": "^0.2.0",
42
+ "turndown": "^7.2.0",
43
+ "turndown-plugin-gfm": "^1.0.2",
44
+ "zod": "^3.25.76"
45
+ },
46
+ "devDependencies": {
47
+ "@types/jsdom": "^21.1.7",
48
+ "@types/node": "^24.0.0",
49
+ "@types/pg": "^8.15.0",
50
+ "@types/turndown": "^5.0.5",
51
+ "tsup": "^8.5.0",
52
+ "tsx": "^4.20.0",
53
+ "typescript": "^5.9.0",
54
+ "vitest": "^3.2.0"
55
+ }
56
+ }