@coffer-org/server 1.7.1 → 1.9.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.
Files changed (61) hide show
  1. package/dist/auth-api.d.ts +1 -0
  2. package/dist/auth-api.js +20 -10
  3. package/dist/background-scheduler.d.ts +22 -0
  4. package/dist/background-scheduler.js +101 -0
  5. package/dist/collection-io.d.ts +7 -7
  6. package/dist/collection-io.js +2 -2
  7. package/dist/connector-identity.d.ts +5 -0
  8. package/dist/connector-identity.js +32 -0
  9. package/dist/embed-openai.d.ts +10 -0
  10. package/dist/embed-openai.js +28 -0
  11. package/dist/entity-schema.d.ts +9 -5
  12. package/dist/entity-schema.js +64 -10
  13. package/dist/extend-io.d.ts +5 -3
  14. package/dist/extend-io.js +17 -10
  15. package/dist/extend-table.d.ts +4 -3
  16. package/dist/extend-table.js +10 -18
  17. package/dist/field-masking.d.ts +7 -0
  18. package/dist/field-masking.js +60 -0
  19. package/dist/index-signal.d.ts +3 -0
  20. package/dist/index-signal.js +14 -0
  21. package/dist/index.js +68 -160
  22. package/dist/local-api.d.ts +3 -3
  23. package/dist/local-api.js +6 -6
  24. package/dist/mcp-http.d.ts +5 -0
  25. package/dist/mcp-http.js +37 -0
  26. package/dist/mcp-http.test-helpers.d.ts +17 -0
  27. package/dist/mcp-http.test-helpers.js +117 -0
  28. package/dist/mcp-local.d.ts +10 -0
  29. package/dist/mcp-local.js +57 -0
  30. package/dist/mcp-tools.d.ts +61 -0
  31. package/dist/mcp-tools.js +225 -0
  32. package/dist/msg-log.d.ts +0 -1
  33. package/dist/msg-log.js +2 -2
  34. package/dist/mutate.d.ts +7 -5
  35. package/dist/mutate.js +20 -8
  36. package/dist/oauth-api.d.ts +2 -0
  37. package/dist/oauth-api.js +281 -0
  38. package/dist/oauth-store.d.ts +56 -0
  39. package/dist/oauth-store.js +159 -0
  40. package/dist/plugin-hooks.d.ts +10 -0
  41. package/dist/plugin-hooks.js +8 -0
  42. package/dist/plugin-runtime.d.ts +1 -0
  43. package/dist/plugin-runtime.js +30 -6
  44. package/dist/plugins-api.d.ts +1 -1
  45. package/dist/plugins-api.js +21 -11
  46. package/dist/public-url.d.ts +4 -0
  47. package/dist/public-url.js +35 -0
  48. package/dist/records-api.d.ts +8 -8
  49. package/dist/records-api.js +35 -32
  50. package/dist/registry-context.d.ts +1 -1
  51. package/dist/registry-context.js +2 -2
  52. package/dist/schema-api.js +5 -5
  53. package/dist/settings-write.d.ts +19 -0
  54. package/dist/settings-write.js +63 -0
  55. package/dist/temporal.d.ts +3 -3
  56. package/dist/temporal.js +1 -1
  57. package/dist/thread-store.d.ts +20 -0
  58. package/dist/thread-store.js +27 -0
  59. package/dist/uploads.d.ts +1 -0
  60. package/dist/uploads.js +4 -0
  61. package/package.json +6 -6
@@ -0,0 +1,60 @@
1
+ import { fieldEntries, collectionGroups } from '@coffer-org/sdk/shelf';
2
+ export const MASK = '********';
3
+ export function passwordKeys(fields) {
4
+ return fieldEntries(fields)
5
+ .filter(([, f]) => f.kind === 'password')
6
+ .map(([k]) => k);
7
+ }
8
+ export function maskSecrets(fields, row) {
9
+ const out = { ...row };
10
+ for (const k of passwordKeys(fields))
11
+ if (out[k])
12
+ out[k] = MASK;
13
+ return out;
14
+ }
15
+ export function preserveSecrets(fields, incoming, existing) {
16
+ const out = { ...incoming };
17
+ for (const k of passwordKeys(fields)) {
18
+ if (out[k] !== MASK)
19
+ continue;
20
+ if (existing[k] !== undefined && existing[k] !== null)
21
+ out[k] = existing[k];
22
+ else
23
+ delete out[k];
24
+ }
25
+ return out;
26
+ }
27
+ export function maskTree(fields, obj) {
28
+ const out = maskSecrets(fields, obj);
29
+ for (const c of collectionGroups(fields)) {
30
+ const key = c.key;
31
+ const sub = c.group.fields;
32
+ const arr = out[key];
33
+ if (Array.isArray(arr)) {
34
+ out[key] = arr.map((item) => item && typeof item === 'object' ? maskTree(sub, item) : item);
35
+ }
36
+ }
37
+ return out;
38
+ }
39
+ export function preserveTree(fields, incoming, existing) {
40
+ const out = preserveSecrets(fields, incoming, existing);
41
+ for (const c of collectionGroups(fields)) {
42
+ const key = c.key;
43
+ const sub = c.group.fields;
44
+ const uniq = c.group.unique ?? [];
45
+ const inArr = out[key];
46
+ if (!Array.isArray(inArr))
47
+ continue;
48
+ const exArr = Array.isArray(existing[key]) ? existing[key] : [];
49
+ out[key] = inArr.map((item) => {
50
+ if (!item || typeof item !== 'object')
51
+ return item;
52
+ const rec = item;
53
+ const match = uniq.length > 0
54
+ ? exArr.find((e) => e && uniq.every((u) => e[u] === rec[u]))
55
+ : undefined;
56
+ return preserveTree(sub, rec, (match ?? {}));
57
+ });
58
+ }
59
+ return out;
60
+ }
@@ -0,0 +1,3 @@
1
+ export declare function onRecordsChanged(cb: () => void): void;
2
+ export declare function offRecordsChanged(): void;
3
+ export declare function notifyRecordsChanged(): void;
@@ -0,0 +1,14 @@
1
+ import { getLogger } from '@coffer-org/sdk/logger';
2
+ const log = getLogger('rag-signal');
3
+ let listener = null;
4
+ export function onRecordsChanged(cb) {
5
+ if (listener)
6
+ log.warn('onRecordsChanged: overwriting an already-registered listener without offRecordsChanged');
7
+ listener = cb;
8
+ }
9
+ export function offRecordsChanged() {
10
+ listener = null;
11
+ }
12
+ export function notifyRecordsChanged() {
13
+ listener?.();
14
+ }
package/dist/index.js CHANGED
@@ -1,11 +1,3 @@
1
- var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
- if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
- return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
- return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
- });
6
- }
7
- return path;
8
- };
9
1
  import { randomUUID } from 'node:crypto';
10
2
  import { createWriteStream, existsSync } from 'node:fs';
11
3
  import { realpath } from 'node:fs/promises';
@@ -17,21 +9,26 @@ import cors from '@fastify/cors';
17
9
  import multipart from '@fastify/multipart';
18
10
  import fastifyStatic from '@fastify/static';
19
11
  import { serialize } from '@mikro-orm/core';
20
- import { textSearchKeys, recordTitle, buildZodObject } from '@coffer-org/sdk/module';
12
+ import { textSearchKeys, recordTitle } from '@coffer-org/sdk/shelf';
21
13
  import { tokenize } from '@coffer-org/core/search';
22
- import { moduleTableName } from "./entity-schema.js";
14
+ import { shelfTableName } from "./entity-schema.js";
23
15
  import { getEm, closeDb } from "./db.js";
24
- import { ValidationError, NotFoundError, toIssue } from "./mutate.js";
16
+ import { ValidationError, NotFoundError } from "./mutate.js";
25
17
  import { resolveEmbed } from "./embed.js";
26
18
  import { resolveWithinHome, filterSort, listDir } from "./fs-list.js";
27
19
  import { initPlugins, teardownPlugins, readDisabled, purgePluginData, getPluginSettings, getPlugins, } from "./plugin-runtime.js";
28
20
  import { registerPluginsApi } from "./plugins-api.js";
21
+ import { pluginHooks, HttpError } from "./plugin-hooks.js";
29
22
  import { registerAuthApi, resolveRequestUser, requireAdmin, PUBLIC_API_PATHS } from "./auth-api.js";
23
+ import { registerMcpHttp } from "./mcp-http.js";
24
+ import { registerOAuthApi } from "./oauth-api.js";
25
+ import { baseUrl } from "./public-url.js";
30
26
  import { discoverPluginAssets, discoverRuntime } from "./plugin-discovery.js";
31
27
  import { checkLatestVersion, resolveUpdateTarget, resolveAllUpdateTargets, resolveRuntimeTarget, runNpmInstall, } from "./plugin-updates.js";
32
28
  import { buildClientSchema } from "./schema-api.js";
33
29
  import { recordList, recordGet, recordCreate, recordUpdate, recordDelete, rowMatch, UnknownTypeError } from "./records-api.js";
34
- import { maskTree, preserveTree } from "./secrets.js";
30
+ import { maskTree, preserveTree } from "./field-masking.js";
31
+ import { writePluginSettings } from "./settings-write.js";
35
32
  import { rootLogger, getLogger } from "./log.js";
36
33
  import { uploadsDir } from "./uploads.js";
37
34
  const ENV_FILE = join(process.cwd(), '.env');
@@ -39,12 +36,12 @@ if (existsSync(ENV_FILE))
39
36
  process.loadEnvFile(ENV_FILE);
40
37
  const PORT = Number(process.env.PORT ?? 7023);
41
38
  const reg = await initPlugins();
42
- const { vaults, modules, getModule, extends_, getExtendsFor } = reg;
39
+ const { libraries, shelves, getShelf, extends_, getExtendsFor } = reg;
43
40
  const entityNames = new Map();
44
- const moduleDefs = new Map();
45
- for (const m of modules) {
46
- entityNames.set(`${m.vault}/${m.module}`, moduleTableName(m.vault, m.module));
47
- moduleDefs.set(`${m.vault}/${m.module}`, m);
41
+ const shelfDefs = new Map();
42
+ for (const m of shelves) {
43
+ entityNames.set(`${m.library}/${m.shelf}`, shelfTableName(m.library, m.shelf));
44
+ shelfDefs.set(`${m.library}/${m.shelf}`, m);
48
45
  }
49
46
  const UPLOADS = uploadsDir();
50
47
  const app = Fastify({
@@ -72,14 +69,19 @@ app.addHook('onSend', async (req, reply, payload) => {
72
69
  return payload;
73
70
  });
74
71
  app.addHook('onRequest', async (req, reply) => {
75
- const gated = req.url.startsWith('/api/') || req.url.startsWith('/uploads/');
72
+ const isMcp = req.url === '/mcp' || req.url.startsWith('/mcp?');
73
+ const gated = req.url.startsWith('/api/') || req.url.startsWith('/uploads/') || isMcp;
76
74
  if (!gated)
77
75
  return;
78
76
  if (PUBLIC_API_PATHS.some((p) => req.url.startsWith(p)))
79
77
  return;
80
78
  const user = await resolveRequestUser(req);
81
- if (!user)
79
+ if (!user) {
80
+ if (isMcp) {
81
+ reply.header('WWW-Authenticate', `Bearer resource_metadata="${await baseUrl(req)}/.well-known/oauth-protected-resource"`);
82
+ }
82
83
  return reply.code(401).send({ error: 'unauthorized' });
84
+ }
83
85
  req.user = user;
84
86
  });
85
87
  app.post('/api/upload', async (req, reply) => {
@@ -100,8 +102,8 @@ app.post('/api/embed', async (req, reply) => {
100
102
  return reply.code(422).send({ error: 'unresolved' });
101
103
  return reply.send(rec);
102
104
  });
103
- function key(vault, type) {
104
- return `${vault}/${type}`;
105
+ function key(library, type) {
106
+ return `${library}/${type}`;
105
107
  }
106
108
  async function guard(reply, fn) {
107
109
  try {
@@ -125,11 +127,11 @@ app.get('/health', async (_req, reply) => {
125
127
  return reply.code(503).send({ status: 'db_unavailable' });
126
128
  }
127
129
  });
128
- app.get('/api/vaults', () => buildClientSchema());
130
+ app.get('/api/libraries', () => buildClientSchema());
129
131
  app.get('/api/counts', async () => {
130
132
  const fork = getEm().fork();
131
133
  const out = {};
132
- for (const [k, mdef] of moduleDefs) {
134
+ for (const [k, mdef] of shelfDefs) {
133
135
  if (mdef.standalone === false)
134
136
  continue;
135
137
  const ename = entityNames.get(k);
@@ -147,7 +149,7 @@ app.get('/api/search', async (req) => {
147
149
  const lim = Math.min(Number(limit) || 20, 100);
148
150
  const fork = getEm().fork();
149
151
  const scored = [];
150
- for (const [k, mdef] of moduleDefs) {
152
+ for (const [k, mdef] of shelfDefs) {
151
153
  if (mdef.standalone === false)
152
154
  continue;
153
155
  const ename = entityNames.get(k);
@@ -155,7 +157,7 @@ app.get('/api/search', async (req) => {
155
157
  continue;
156
158
  if (!textSearchKeys(mdef).length)
157
159
  continue;
158
- const [vault, type] = k.split('/');
160
+ const [library, type] = k.split('/');
159
161
  const rows = (await fork.find(ename, {}, { limit: 1000 })).map((r) => serialize(r));
160
162
  for (const row of rows) {
161
163
  const m = rowMatch(mdef, row, tokens);
@@ -164,7 +166,7 @@ app.get('/api/search', async (req) => {
164
166
  const label = recordTitle(mdef, row);
165
167
  scored.push({
166
168
  score: m.score,
167
- result: { vault, type, id: String(row.id), label, snippet: m.snippet },
169
+ result: { library, type, id: String(row.id), label, snippet: m.snippet },
168
170
  });
169
171
  }
170
172
  }
@@ -291,123 +293,27 @@ app.put('/api/plugins/:id/settings', (req, reply) => {
291
293
  if (!p || !p.settings || p.settings.fields.length === 0) {
292
294
  return reply.code(404).send({ error: 'no_settings' });
293
295
  }
294
- const syntheticMod = {
295
- vault: '_settings',
296
- module: id,
297
- label: `${id}.plugin.label`,
298
- fields: p.settings.fields,
299
- };
300
- const existing = await getPluginSettings(id);
301
- const body = preserveTree(p.settings.fields, (req.body ?? {}), existing);
302
- const parsed = buildZodObject(syntheticMod).safeParse(body);
303
- if (!parsed.success) {
304
- throw new ValidationError(parsed.error.issues.map(toIssue));
305
- }
306
- const entityName = `_settings__${id}`;
307
- const row = { plugin_id: id, ...parsed.data };
308
- await getEm()
309
- .fork()
310
- .upsert(entityName, row);
311
- return { ok: true, row: maskTree(p.settings.fields, row) };
296
+ const row = await writePluginSettings(getEm().fork(), id, (req.body ?? {}), req.user.login, plugins);
297
+ return { ok: true, row };
312
298
  });
313
299
  });
314
- app.post('/api/plugins/finance/test', async (req, reply) => {
315
- if (!requireAdmin(req, reply))
316
- return;
317
- const settings = await getPluginSettings('finance');
318
- const url = settings['firefly_url'];
319
- const token = settings['token'];
320
- if (!url || !token) {
321
- return reply.code(400).send({ error: 'Finance settings not configured. Save them first.' });
322
- }
323
- const finRuntime = '@coffer-org/plugin-finance/runtime';
324
- const { checkFireflyConnection } = (await import(__rewriteRelativeImportExtension(finRuntime)));
325
- const result = await checkFireflyConnection(url, token);
326
- if (result.ok)
327
- return { ok: true };
328
- return reply.code(400).send({ error: result.error });
329
- });
330
- app.post('/api/plugins/finance/sync', async (req, reply) => {
331
- if (!requireAdmin(req, reply))
332
- return;
333
- const finRuntime = '@coffer-org/plugin-finance/runtime';
334
- const { runSync } = (await import(__rewriteRelativeImportExtension(finRuntime)));
335
- try {
336
- const counts = await runSync();
337
- return { ok: true, ...counts };
338
- }
339
- catch (e) {
340
- return reply.code(400).send({ error: e.message });
341
- }
342
- });
343
- app.post('/api/plugins/finance/import-wise', async (req, reply) => {
344
- if (!requireAdmin(req, reply))
345
- return;
346
- const finRuntime = '@coffer-org/plugin-finance/runtime';
347
- const { importWise } = (await import(__rewriteRelativeImportExtension(finRuntime)));
348
- try {
349
- return await importWise();
350
- }
351
- catch (e) {
352
- return reply.code(400).send({ error: e.message });
353
- }
354
- });
355
- app.post('/api/plugins/devices/scan', async (req, reply) => {
300
+ app.post('/api/plugins/:id/:action', async (req, reply) => {
356
301
  if (!requireAdmin(req, reply))
357
302
  return;
358
- const devRuntime = '@coffer-org/plugin-devices/runtime';
359
- const { runScan } = (await import(__rewriteRelativeImportExtension(devRuntime)));
303
+ const { id, action } = req.params;
304
+ const fn = pluginHooks[id]?.actions?.[action];
305
+ if (!fn)
306
+ return reply.code(404).send({ error: `unknown action ${id}/${action}` });
360
307
  try {
361
- const result = await runScan({ create: true });
362
- return { ok: true, result };
308
+ return await fn((req.body ?? {}));
363
309
  }
364
310
  catch (e) {
365
311
  if (e instanceof ValidationError) {
366
312
  const detail = e.issues.map((i) => `${i.path.join('.') || i.field}: ${i.code}`).join('; ');
367
- return reply.code(422).send({ error: `Validation error during scan — ${detail}`, issues: e.issues });
313
+ return reply.code(422).send({ error: `Validation error — ${detail}`, issues: e.issues });
368
314
  }
369
- return reply.code(409).send({ error: e.message });
370
- }
371
- });
372
- app.post('/api/plugins/media/test', async (req, reply) => {
373
- if (!requireAdmin(req, reply))
374
- return;
375
- const settings = await getPluginSettings('media');
376
- const url = settings['jellyfin_url'];
377
- const token = settings['jellyfin_token'];
378
- if (!url || !token) {
379
- return reply.code(400).send({ error: 'Jellyfin settings not configured. Save them first.' });
380
- }
381
- const mediaRuntime = '@coffer-org/plugin-media/runtime';
382
- const { testConnection } = (await import(__rewriteRelativeImportExtension(mediaRuntime)));
383
- const result = await testConnection(url, token);
384
- if (result.ok)
385
- return { ok: true };
386
- return reply.code(400).send({ error: result.error });
387
- });
388
- app.post('/api/plugins/media/sync', async (req, reply) => {
389
- if (!requireAdmin(req, reply))
390
- return;
391
- const mediaRuntime = '@coffer-org/plugin-media/runtime';
392
- const { runJellyfinSync } = (await import(__rewriteRelativeImportExtension(mediaRuntime)));
393
- try {
394
- const counts = await runJellyfinSync();
395
- return { ok: true, ...counts };
396
- }
397
- catch (e) {
398
- return reply.code(400).send({ error: e.message });
399
- }
400
- });
401
- app.post('/api/plugins/media/add', async (req, reply) => {
402
- if (!requireAdmin(req, reply))
403
- return;
404
- const { query, tmdb_id, kind } = (req.body ?? {});
405
- const mediaRuntime = '@coffer-org/plugin-media/runtime';
406
- const { addTitle } = (await import(__rewriteRelativeImportExtension(mediaRuntime)));
407
- try {
408
- return await addTitle(query, tmdb_id, kind);
409
- }
410
- catch (e) {
315
+ if (e instanceof HttpError)
316
+ return reply.code(e.status).send({ error: e.message });
411
317
  return reply.code(400).send({ error: e.message });
412
318
  }
413
319
  });
@@ -434,12 +340,12 @@ app.get('/api/fs/list', async (req, reply) => {
434
340
  return { dir: abs, entries: [] };
435
341
  }
436
342
  });
437
- function maskRecordRow(vault, type, row) {
438
- const m = getModule(vault, type);
343
+ function maskRecordRow(library, type, row) {
344
+ const m = getShelf(library, type);
439
345
  let out = m ? maskTree(m.fields, row) : row;
440
346
  const ext = out._extends;
441
347
  if (ext) {
442
- const defs = getExtendsFor(vault, type);
348
+ const defs = getExtendsFor(library, type);
443
349
  const maskedExt = {};
444
350
  for (const [id, sub] of Object.entries(ext)) {
445
351
  const def = defs.find((e) => e.id === id);
@@ -449,12 +355,12 @@ function maskRecordRow(vault, type, row) {
449
355
  }
450
356
  return out;
451
357
  }
452
- app.get('/api/:vault/:type', async (req, reply) => {
453
- const { vault, type } = req.params;
358
+ app.get('/api/:library/:type', async (req, reply) => {
359
+ const { library, type } = req.params;
454
360
  const query = req.query;
455
361
  try {
456
- const rows = await recordList(vault, type, query);
457
- return rows.map((r) => maskRecordRow(vault, type, r));
362
+ const rows = await recordList(library, type, query);
363
+ return rows.map((r) => maskRecordRow(library, type, r));
458
364
  }
459
365
  catch (e) {
460
366
  if (e instanceof UnknownTypeError)
@@ -462,16 +368,16 @@ app.get('/api/:vault/:type', async (req, reply) => {
462
368
  throw e;
463
369
  }
464
370
  });
465
- app.get('/api/:vault/:type/:id', async (req, reply) => {
466
- const { vault, type, id } = req.params;
371
+ app.get('/api/:library/:type/:id', async (req, reply) => {
372
+ const { library, type, id } = req.params;
467
373
  const rid = Number(id);
468
374
  if (isNaN(rid))
469
375
  return reply.code(400).send({ error: 'invalid_id' });
470
376
  try {
471
- const row = await recordGet(vault, type, rid);
377
+ const row = await recordGet(library, type, rid);
472
378
  if (!row)
473
379
  return reply.code(404).send({ error: 'not_found' });
474
- return maskRecordRow(vault, type, row);
380
+ return maskRecordRow(library, type, row);
475
381
  }
476
382
  catch (e) {
477
383
  if (e instanceof UnknownTypeError)
@@ -479,15 +385,15 @@ app.get('/api/:vault/:type/:id', async (req, reply) => {
479
385
  throw e;
480
386
  }
481
387
  });
482
- app.post('/api/:vault/:type', (req, reply) => guard(reply, async () => {
483
- const { vault, type } = req.params;
388
+ app.post('/api/:library/:type', (req, reply) => guard(reply, async () => {
389
+ const { library, type } = req.params;
484
390
  try {
485
- const m = getModule(vault, type);
391
+ const m = getShelf(library, type);
486
392
  let body = (req.body ?? {});
487
393
  if (m)
488
394
  body = preserveTree(m.fields, body, {});
489
- const row = await recordCreate(vault, type, body);
490
- return reply.code(201).send(maskRecordRow(vault, type, row));
395
+ const row = await recordCreate(library, type, body);
396
+ return reply.code(201).send(maskRecordRow(library, type, row));
491
397
  }
492
398
  catch (e) {
493
399
  if (e instanceof UnknownTypeError)
@@ -495,18 +401,18 @@ app.post('/api/:vault/:type', (req, reply) => guard(reply, async () => {
495
401
  throw e;
496
402
  }
497
403
  }));
498
- app.patch('/api/:vault/:type/:id', (req, reply) => guard(reply, async () => {
499
- const { vault, type, id } = req.params;
404
+ app.patch('/api/:library/:type/:id', (req, reply) => guard(reply, async () => {
405
+ const { library, type, id } = req.params;
500
406
  const rid = Number(id);
501
407
  if (isNaN(rid))
502
408
  return reply.code(400).send({ error: 'invalid_id' });
503
409
  try {
504
- const m = getModule(vault, type);
410
+ const m = getShelf(library, type);
505
411
  let body = (req.body ?? {});
506
412
  if (m) {
507
- const existing = (await recordGet(vault, type, rid)) ?? {};
413
+ const existing = (await recordGet(library, type, rid)) ?? {};
508
414
  body = preserveTree(m.fields, body, existing);
509
- const exDefs = getExtendsFor(vault, type);
415
+ const exDefs = getExtendsFor(library, type);
510
416
  const exRecs = (existing._extends ?? {});
511
417
  for (const def of exDefs) {
512
418
  const bk = `_extend_${def.id}`;
@@ -516,8 +422,8 @@ app.patch('/api/:vault/:type/:id', (req, reply) => guard(reply, async () => {
516
422
  }
517
423
  }
518
424
  }
519
- const row = await recordUpdate(vault, type, rid, body);
520
- return maskRecordRow(vault, type, row);
425
+ const row = await recordUpdate(library, type, rid, body);
426
+ return maskRecordRow(library, type, row);
521
427
  }
522
428
  catch (e) {
523
429
  if (e instanceof UnknownTypeError)
@@ -525,13 +431,13 @@ app.patch('/api/:vault/:type/:id', (req, reply) => guard(reply, async () => {
525
431
  throw e;
526
432
  }
527
433
  }));
528
- app.delete('/api/:vault/:type/:id', (req, reply) => guard(reply, async () => {
529
- const { vault, type, id } = req.params;
434
+ app.delete('/api/:library/:type/:id', (req, reply) => guard(reply, async () => {
435
+ const { library, type, id } = req.params;
530
436
  const rid = Number(id);
531
437
  if (isNaN(rid))
532
438
  return reply.code(400).send({ error: 'invalid_id' });
533
439
  try {
534
- await recordDelete(vault, type, rid);
440
+ await recordDelete(library, type, rid);
535
441
  return reply.code(204).send();
536
442
  }
537
443
  catch (e) {
@@ -541,6 +447,8 @@ app.delete('/api/:vault/:type/:id', (req, reply) => guard(reply, async () => {
541
447
  }
542
448
  }));
543
449
  await registerAuthApi(app);
450
+ registerOAuthApi(app);
451
+ await registerMcpHttp(app);
544
452
  await registerPluginsApi(app);
545
453
  const WEB_DIST = process.env.WEB_DIST;
546
454
  if (WEB_DIST && existsSync(join(WEB_DIST, 'index.html'))) {
@@ -562,7 +470,7 @@ if (WEB_DIST && existsSync(join(WEB_DIST, 'index.html'))) {
562
470
  }
563
471
  app
564
472
  .listen({ port: PORT, host: '0.0.0.0' })
565
- .then(() => app.log.info(`vault-server :${PORT}`))
473
+ .then(() => app.log.info(`library-server :${PORT}`))
566
474
  .catch((e) => {
567
475
  app.log.error(e);
568
476
  process.exit(1);
@@ -1,5 +1,5 @@
1
1
  import { UnknownTypeError } from './records-api.ts';
2
2
  export { UnknownTypeError };
3
- export declare function localExists(vault: string, type: string, id: number): Promise<boolean>;
4
- export declare function localPost(vault: string, type: string, body: unknown): Promise<Record<string, unknown>>;
5
- export declare function localPatch(vault: string, type: string, id: number, body: unknown): Promise<Record<string, unknown>>;
3
+ export declare function localExists(library: string, type: string, id: number): Promise<boolean>;
4
+ export declare function localPost(library: string, type: string, body: unknown): Promise<Record<string, unknown>>;
5
+ export declare function localPatch(library: string, type: string, id: number, body: unknown): Promise<Record<string, unknown>>;
package/dist/local-api.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { recordCreate, recordUpdate, recordGet, UnknownTypeError } from "./records-api.js";
2
2
  export { UnknownTypeError };
3
- export async function localExists(vault, type, id) {
3
+ export async function localExists(library, type, id) {
4
4
  try {
5
- return Boolean(await recordGet(vault, type, id));
5
+ return Boolean(await recordGet(library, type, id));
6
6
  }
7
7
  catch (e) {
8
8
  if (e instanceof UnknownTypeError)
@@ -10,9 +10,9 @@ export async function localExists(vault, type, id) {
10
10
  throw e;
11
11
  }
12
12
  }
13
- export function localPost(vault, type, body) {
14
- return recordCreate(vault, type, body, 'import');
13
+ export function localPost(library, type, body) {
14
+ return recordCreate(library, type, body, 'import');
15
15
  }
16
- export function localPatch(vault, type, id, body) {
17
- return recordUpdate(vault, type, id, body, 'import');
16
+ export function localPatch(library, type, id, body) {
17
+ return recordUpdate(library, type, id, body, 'import');
18
18
  }
@@ -0,0 +1,5 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import type { AuthRole } from './plugin-hooks.ts';
4
+ export declare function buildMcpServer(role: AuthRole, actor: string): Promise<McpServer>;
5
+ export declare function registerMcpHttp(app: FastifyInstance): Promise<void>;
@@ -0,0 +1,37 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3
+ import { collectMcpTools, resolveRagDeps, buildDomainSections, buildMcpInstructions } from "./mcp-tools.js";
4
+ import { getLogger } from "./log.js";
5
+ const log = getLogger('mcp-http');
6
+ export async function buildMcpServer(role, actor) {
7
+ const rag = await resolveRagDeps();
8
+ const tools = (await collectMcpTools({ rag, includeAdmin: role === 'admin', actor })).filter((t) => role === 'admin' || t.role === 'member');
9
+ const sections = await buildDomainSections();
10
+ const server = new McpServer({ name: 'coffer', version: '1.0.0' }, { instructions: buildMcpInstructions(sections) });
11
+ const registerTool = server.registerTool.bind(server);
12
+ for (const t of tools) {
13
+ registerTool(t.httpName, { description: t.description, inputSchema: t.inputSchema }, async (args) => t.handler(args));
14
+ }
15
+ return server;
16
+ }
17
+ export async function registerMcpHttp(app) {
18
+ app.all('/mcp', async (req, reply) => {
19
+ const role = req.user.role;
20
+ const actor = req.user.login;
21
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
22
+ reply.hijack();
23
+ reply.raw.on('close', () => void transport.close());
24
+ try {
25
+ const server = await buildMcpServer(role, actor);
26
+ await server.connect(transport);
27
+ await transport.handleRequest(req.raw, reply.raw, req.body);
28
+ }
29
+ catch (e) {
30
+ log.error(`/mcp handler error: ${e.message}`);
31
+ if (!reply.raw.headersSent) {
32
+ reply.raw.writeHead(500, { 'content-type': 'application/json' });
33
+ reply.raw.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32603, message: 'internal' }, id: null }));
34
+ }
35
+ }
36
+ });
37
+ }
@@ -0,0 +1,17 @@
1
+ import { type FastifyInstance } from 'fastify';
2
+ import { closeDb } from './db.ts';
3
+ export declare function freshMcpApp(opts?: {
4
+ withAdminTool?: boolean;
5
+ withInstructions?: boolean;
6
+ }): Promise<FastifyInstance>;
7
+ export declare function adminToken(): Promise<string>;
8
+ export declare function memberToken(): Promise<string>;
9
+ export declare function parseRpcResult(res: {
10
+ body: string;
11
+ headers: Record<string, unknown>;
12
+ }): Record<string, unknown> | undefined;
13
+ export declare function parseToolNames(res: {
14
+ body: string;
15
+ headers: Record<string, unknown>;
16
+ }): string[];
17
+ export { closeDb };