@coffer-org/server 1.7.1 → 1.8.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/auth-api.js +3 -3
- package/dist/background-scheduler.d.ts +22 -0
- package/dist/background-scheduler.js +101 -0
- package/dist/collection-io.d.ts +7 -7
- package/dist/collection-io.js +2 -2
- package/dist/connector-identity.d.ts +5 -0
- package/dist/connector-identity.js +32 -0
- package/dist/embed-openai.d.ts +10 -0
- package/dist/embed-openai.js +28 -0
- package/dist/entity-schema.d.ts +6 -5
- package/dist/entity-schema.js +24 -10
- package/dist/extend-io.d.ts +5 -3
- package/dist/extend-io.js +17 -10
- package/dist/extend-table.d.ts +4 -3
- package/dist/extend-table.js +10 -18
- package/dist/field-masking.d.ts +7 -0
- package/dist/field-masking.js +60 -0
- package/dist/index-signal.d.ts +3 -0
- package/dist/index-signal.js +14 -0
- package/dist/index.js +59 -159
- package/dist/local-api.d.ts +3 -3
- package/dist/local-api.js +6 -6
- package/dist/mcp-http.d.ts +5 -0
- package/dist/mcp-http.js +37 -0
- package/dist/mcp-http.test-helpers.d.ts +17 -0
- package/dist/mcp-http.test-helpers.js +117 -0
- package/dist/mcp-local.d.ts +10 -0
- package/dist/mcp-local.js +57 -0
- package/dist/mcp-tools.d.ts +61 -0
- package/dist/mcp-tools.js +225 -0
- package/dist/msg-log.d.ts +0 -1
- package/dist/msg-log.js +2 -2
- package/dist/mutate.d.ts +7 -5
- package/dist/mutate.js +20 -8
- package/dist/plugin-hooks.d.ts +10 -0
- package/dist/plugin-hooks.js +8 -0
- package/dist/plugin-runtime.d.ts +1 -0
- package/dist/plugin-runtime.js +30 -6
- package/dist/plugins-api.d.ts +1 -1
- package/dist/plugins-api.js +21 -11
- package/dist/records-api.d.ts +8 -8
- package/dist/records-api.js +35 -32
- package/dist/registry-context.d.ts +1 -1
- package/dist/registry-context.js +2 -2
- package/dist/schema-api.js +5 -5
- package/dist/settings-write.d.ts +19 -0
- package/dist/settings-write.js +60 -0
- package/dist/temporal.d.ts +3 -3
- package/dist/temporal.js +1 -1
- package/dist/thread-store.d.ts +20 -0
- package/dist/thread-store.js +27 -0
- package/dist/uploads.d.ts +1 -0
- package/dist/uploads.js +4 -0
- package/package.json +6 -6
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,24 @@ 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
|
|
12
|
+
import { textSearchKeys, recordTitle } from '@coffer-org/sdk/shelf';
|
|
21
13
|
import { tokenize } from '@coffer-org/core/search';
|
|
22
|
-
import {
|
|
14
|
+
import { shelfTableName } from "./entity-schema.js";
|
|
23
15
|
import { getEm, closeDb } from "./db.js";
|
|
24
|
-
import { ValidationError, NotFoundError
|
|
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";
|
|
30
24
|
import { discoverPluginAssets, discoverRuntime } from "./plugin-discovery.js";
|
|
31
25
|
import { checkLatestVersion, resolveUpdateTarget, resolveAllUpdateTargets, resolveRuntimeTarget, runNpmInstall, } from "./plugin-updates.js";
|
|
32
26
|
import { buildClientSchema } from "./schema-api.js";
|
|
33
27
|
import { recordList, recordGet, recordCreate, recordUpdate, recordDelete, rowMatch, UnknownTypeError } from "./records-api.js";
|
|
34
|
-
import { maskTree, preserveTree } from "./
|
|
28
|
+
import { maskTree, preserveTree } from "./field-masking.js";
|
|
29
|
+
import { writePluginSettings } from "./settings-write.js";
|
|
35
30
|
import { rootLogger, getLogger } from "./log.js";
|
|
36
31
|
import { uploadsDir } from "./uploads.js";
|
|
37
32
|
const ENV_FILE = join(process.cwd(), '.env');
|
|
@@ -39,12 +34,12 @@ if (existsSync(ENV_FILE))
|
|
|
39
34
|
process.loadEnvFile(ENV_FILE);
|
|
40
35
|
const PORT = Number(process.env.PORT ?? 7023);
|
|
41
36
|
const reg = await initPlugins();
|
|
42
|
-
const {
|
|
37
|
+
const { libraries, shelves, getShelf, extends_, getExtendsFor } = reg;
|
|
43
38
|
const entityNames = new Map();
|
|
44
|
-
const
|
|
45
|
-
for (const m of
|
|
46
|
-
entityNames.set(`${m.
|
|
47
|
-
|
|
39
|
+
const shelfDefs = new Map();
|
|
40
|
+
for (const m of shelves) {
|
|
41
|
+
entityNames.set(`${m.library}/${m.shelf}`, shelfTableName(m.library, m.shelf));
|
|
42
|
+
shelfDefs.set(`${m.library}/${m.shelf}`, m);
|
|
48
43
|
}
|
|
49
44
|
const UPLOADS = uploadsDir();
|
|
50
45
|
const app = Fastify({
|
|
@@ -72,7 +67,7 @@ app.addHook('onSend', async (req, reply, payload) => {
|
|
|
72
67
|
return payload;
|
|
73
68
|
});
|
|
74
69
|
app.addHook('onRequest', async (req, reply) => {
|
|
75
|
-
const gated = req.url.startsWith('/api/') || req.url.startsWith('/uploads/');
|
|
70
|
+
const gated = req.url.startsWith('/api/') || req.url.startsWith('/uploads/') || req.url === '/mcp' || req.url.startsWith('/mcp?');
|
|
76
71
|
if (!gated)
|
|
77
72
|
return;
|
|
78
73
|
if (PUBLIC_API_PATHS.some((p) => req.url.startsWith(p)))
|
|
@@ -100,8 +95,8 @@ app.post('/api/embed', async (req, reply) => {
|
|
|
100
95
|
return reply.code(422).send({ error: 'unresolved' });
|
|
101
96
|
return reply.send(rec);
|
|
102
97
|
});
|
|
103
|
-
function key(
|
|
104
|
-
return `${
|
|
98
|
+
function key(library, type) {
|
|
99
|
+
return `${library}/${type}`;
|
|
105
100
|
}
|
|
106
101
|
async function guard(reply, fn) {
|
|
107
102
|
try {
|
|
@@ -125,11 +120,11 @@ app.get('/health', async (_req, reply) => {
|
|
|
125
120
|
return reply.code(503).send({ status: 'db_unavailable' });
|
|
126
121
|
}
|
|
127
122
|
});
|
|
128
|
-
app.get('/api/
|
|
123
|
+
app.get('/api/libraries', () => buildClientSchema());
|
|
129
124
|
app.get('/api/counts', async () => {
|
|
130
125
|
const fork = getEm().fork();
|
|
131
126
|
const out = {};
|
|
132
|
-
for (const [k, mdef] of
|
|
127
|
+
for (const [k, mdef] of shelfDefs) {
|
|
133
128
|
if (mdef.standalone === false)
|
|
134
129
|
continue;
|
|
135
130
|
const ename = entityNames.get(k);
|
|
@@ -147,7 +142,7 @@ app.get('/api/search', async (req) => {
|
|
|
147
142
|
const lim = Math.min(Number(limit) || 20, 100);
|
|
148
143
|
const fork = getEm().fork();
|
|
149
144
|
const scored = [];
|
|
150
|
-
for (const [k, mdef] of
|
|
145
|
+
for (const [k, mdef] of shelfDefs) {
|
|
151
146
|
if (mdef.standalone === false)
|
|
152
147
|
continue;
|
|
153
148
|
const ename = entityNames.get(k);
|
|
@@ -155,7 +150,7 @@ app.get('/api/search', async (req) => {
|
|
|
155
150
|
continue;
|
|
156
151
|
if (!textSearchKeys(mdef).length)
|
|
157
152
|
continue;
|
|
158
|
-
const [
|
|
153
|
+
const [library, type] = k.split('/');
|
|
159
154
|
const rows = (await fork.find(ename, {}, { limit: 1000 })).map((r) => serialize(r));
|
|
160
155
|
for (const row of rows) {
|
|
161
156
|
const m = rowMatch(mdef, row, tokens);
|
|
@@ -164,7 +159,7 @@ app.get('/api/search', async (req) => {
|
|
|
164
159
|
const label = recordTitle(mdef, row);
|
|
165
160
|
scored.push({
|
|
166
161
|
score: m.score,
|
|
167
|
-
result: {
|
|
162
|
+
result: { library, type, id: String(row.id), label, snippet: m.snippet },
|
|
168
163
|
});
|
|
169
164
|
}
|
|
170
165
|
}
|
|
@@ -291,123 +286,27 @@ app.put('/api/plugins/:id/settings', (req, reply) => {
|
|
|
291
286
|
if (!p || !p.settings || p.settings.fields.length === 0) {
|
|
292
287
|
return reply.code(404).send({ error: 'no_settings' });
|
|
293
288
|
}
|
|
294
|
-
const
|
|
295
|
-
|
|
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) };
|
|
289
|
+
const row = await writePluginSettings(getEm().fork(), id, (req.body ?? {}), req.user.login, plugins);
|
|
290
|
+
return { ok: true, row };
|
|
312
291
|
});
|
|
313
292
|
});
|
|
314
|
-
app.post('/api/plugins
|
|
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) => {
|
|
293
|
+
app.post('/api/plugins/:id/:action', async (req, reply) => {
|
|
356
294
|
if (!requireAdmin(req, reply))
|
|
357
295
|
return;
|
|
358
|
-
const
|
|
359
|
-
const
|
|
296
|
+
const { id, action } = req.params;
|
|
297
|
+
const fn = pluginHooks[id]?.actions?.[action];
|
|
298
|
+
if (!fn)
|
|
299
|
+
return reply.code(404).send({ error: `unknown action ${id}/${action}` });
|
|
360
300
|
try {
|
|
361
|
-
|
|
362
|
-
return { ok: true, result };
|
|
301
|
+
return await fn((req.body ?? {}));
|
|
363
302
|
}
|
|
364
303
|
catch (e) {
|
|
365
304
|
if (e instanceof ValidationError) {
|
|
366
305
|
const detail = e.issues.map((i) => `${i.path.join('.') || i.field}: ${i.code}`).join('; ');
|
|
367
|
-
return reply.code(422).send({ error: `Validation error
|
|
306
|
+
return reply.code(422).send({ error: `Validation error — ${detail}`, issues: e.issues });
|
|
368
307
|
}
|
|
369
|
-
|
|
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) {
|
|
308
|
+
if (e instanceof HttpError)
|
|
309
|
+
return reply.code(e.status).send({ error: e.message });
|
|
411
310
|
return reply.code(400).send({ error: e.message });
|
|
412
311
|
}
|
|
413
312
|
});
|
|
@@ -434,12 +333,12 @@ app.get('/api/fs/list', async (req, reply) => {
|
|
|
434
333
|
return { dir: abs, entries: [] };
|
|
435
334
|
}
|
|
436
335
|
});
|
|
437
|
-
function maskRecordRow(
|
|
438
|
-
const m =
|
|
336
|
+
function maskRecordRow(library, type, row) {
|
|
337
|
+
const m = getShelf(library, type);
|
|
439
338
|
let out = m ? maskTree(m.fields, row) : row;
|
|
440
339
|
const ext = out._extends;
|
|
441
340
|
if (ext) {
|
|
442
|
-
const defs = getExtendsFor(
|
|
341
|
+
const defs = getExtendsFor(library, type);
|
|
443
342
|
const maskedExt = {};
|
|
444
343
|
for (const [id, sub] of Object.entries(ext)) {
|
|
445
344
|
const def = defs.find((e) => e.id === id);
|
|
@@ -449,12 +348,12 @@ function maskRecordRow(vault, type, row) {
|
|
|
449
348
|
}
|
|
450
349
|
return out;
|
|
451
350
|
}
|
|
452
|
-
app.get('/api/:
|
|
453
|
-
const {
|
|
351
|
+
app.get('/api/:library/:type', async (req, reply) => {
|
|
352
|
+
const { library, type } = req.params;
|
|
454
353
|
const query = req.query;
|
|
455
354
|
try {
|
|
456
|
-
const rows = await recordList(
|
|
457
|
-
return rows.map((r) => maskRecordRow(
|
|
355
|
+
const rows = await recordList(library, type, query);
|
|
356
|
+
return rows.map((r) => maskRecordRow(library, type, r));
|
|
458
357
|
}
|
|
459
358
|
catch (e) {
|
|
460
359
|
if (e instanceof UnknownTypeError)
|
|
@@ -462,16 +361,16 @@ app.get('/api/:vault/:type', async (req, reply) => {
|
|
|
462
361
|
throw e;
|
|
463
362
|
}
|
|
464
363
|
});
|
|
465
|
-
app.get('/api/:
|
|
466
|
-
const {
|
|
364
|
+
app.get('/api/:library/:type/:id', async (req, reply) => {
|
|
365
|
+
const { library, type, id } = req.params;
|
|
467
366
|
const rid = Number(id);
|
|
468
367
|
if (isNaN(rid))
|
|
469
368
|
return reply.code(400).send({ error: 'invalid_id' });
|
|
470
369
|
try {
|
|
471
|
-
const row = await recordGet(
|
|
370
|
+
const row = await recordGet(library, type, rid);
|
|
472
371
|
if (!row)
|
|
473
372
|
return reply.code(404).send({ error: 'not_found' });
|
|
474
|
-
return maskRecordRow(
|
|
373
|
+
return maskRecordRow(library, type, row);
|
|
475
374
|
}
|
|
476
375
|
catch (e) {
|
|
477
376
|
if (e instanceof UnknownTypeError)
|
|
@@ -479,15 +378,15 @@ app.get('/api/:vault/:type/:id', async (req, reply) => {
|
|
|
479
378
|
throw e;
|
|
480
379
|
}
|
|
481
380
|
});
|
|
482
|
-
app.post('/api/:
|
|
483
|
-
const {
|
|
381
|
+
app.post('/api/:library/:type', (req, reply) => guard(reply, async () => {
|
|
382
|
+
const { library, type } = req.params;
|
|
484
383
|
try {
|
|
485
|
-
const m =
|
|
384
|
+
const m = getShelf(library, type);
|
|
486
385
|
let body = (req.body ?? {});
|
|
487
386
|
if (m)
|
|
488
387
|
body = preserveTree(m.fields, body, {});
|
|
489
|
-
const row = await recordCreate(
|
|
490
|
-
return reply.code(201).send(maskRecordRow(
|
|
388
|
+
const row = await recordCreate(library, type, body);
|
|
389
|
+
return reply.code(201).send(maskRecordRow(library, type, row));
|
|
491
390
|
}
|
|
492
391
|
catch (e) {
|
|
493
392
|
if (e instanceof UnknownTypeError)
|
|
@@ -495,18 +394,18 @@ app.post('/api/:vault/:type', (req, reply) => guard(reply, async () => {
|
|
|
495
394
|
throw e;
|
|
496
395
|
}
|
|
497
396
|
}));
|
|
498
|
-
app.patch('/api/:
|
|
499
|
-
const {
|
|
397
|
+
app.patch('/api/:library/:type/:id', (req, reply) => guard(reply, async () => {
|
|
398
|
+
const { library, type, id } = req.params;
|
|
500
399
|
const rid = Number(id);
|
|
501
400
|
if (isNaN(rid))
|
|
502
401
|
return reply.code(400).send({ error: 'invalid_id' });
|
|
503
402
|
try {
|
|
504
|
-
const m =
|
|
403
|
+
const m = getShelf(library, type);
|
|
505
404
|
let body = (req.body ?? {});
|
|
506
405
|
if (m) {
|
|
507
|
-
const existing = (await recordGet(
|
|
406
|
+
const existing = (await recordGet(library, type, rid)) ?? {};
|
|
508
407
|
body = preserveTree(m.fields, body, existing);
|
|
509
|
-
const exDefs = getExtendsFor(
|
|
408
|
+
const exDefs = getExtendsFor(library, type);
|
|
510
409
|
const exRecs = (existing._extends ?? {});
|
|
511
410
|
for (const def of exDefs) {
|
|
512
411
|
const bk = `_extend_${def.id}`;
|
|
@@ -516,8 +415,8 @@ app.patch('/api/:vault/:type/:id', (req, reply) => guard(reply, async () => {
|
|
|
516
415
|
}
|
|
517
416
|
}
|
|
518
417
|
}
|
|
519
|
-
const row = await recordUpdate(
|
|
520
|
-
return maskRecordRow(
|
|
418
|
+
const row = await recordUpdate(library, type, rid, body);
|
|
419
|
+
return maskRecordRow(library, type, row);
|
|
521
420
|
}
|
|
522
421
|
catch (e) {
|
|
523
422
|
if (e instanceof UnknownTypeError)
|
|
@@ -525,13 +424,13 @@ app.patch('/api/:vault/:type/:id', (req, reply) => guard(reply, async () => {
|
|
|
525
424
|
throw e;
|
|
526
425
|
}
|
|
527
426
|
}));
|
|
528
|
-
app.delete('/api/:
|
|
529
|
-
const {
|
|
427
|
+
app.delete('/api/:library/:type/:id', (req, reply) => guard(reply, async () => {
|
|
428
|
+
const { library, type, id } = req.params;
|
|
530
429
|
const rid = Number(id);
|
|
531
430
|
if (isNaN(rid))
|
|
532
431
|
return reply.code(400).send({ error: 'invalid_id' });
|
|
533
432
|
try {
|
|
534
|
-
await recordDelete(
|
|
433
|
+
await recordDelete(library, type, rid);
|
|
535
434
|
return reply.code(204).send();
|
|
536
435
|
}
|
|
537
436
|
catch (e) {
|
|
@@ -541,6 +440,7 @@ app.delete('/api/:vault/:type/:id', (req, reply) => guard(reply, async () => {
|
|
|
541
440
|
}
|
|
542
441
|
}));
|
|
543
442
|
await registerAuthApi(app);
|
|
443
|
+
await registerMcpHttp(app);
|
|
544
444
|
await registerPluginsApi(app);
|
|
545
445
|
const WEB_DIST = process.env.WEB_DIST;
|
|
546
446
|
if (WEB_DIST && existsSync(join(WEB_DIST, 'index.html'))) {
|
|
@@ -562,7 +462,7 @@ if (WEB_DIST && existsSync(join(WEB_DIST, 'index.html'))) {
|
|
|
562
462
|
}
|
|
563
463
|
app
|
|
564
464
|
.listen({ port: PORT, host: '0.0.0.0' })
|
|
565
|
-
.then(() => app.log.info(`
|
|
465
|
+
.then(() => app.log.info(`library-server :${PORT}`))
|
|
566
466
|
.catch((e) => {
|
|
567
467
|
app.log.error(e);
|
|
568
468
|
process.exit(1);
|
package/dist/local-api.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { UnknownTypeError } from './records-api.ts';
|
|
2
2
|
export { UnknownTypeError };
|
|
3
|
-
export declare function localExists(
|
|
4
|
-
export declare function localPost(
|
|
5
|
-
export declare function localPatch(
|
|
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(
|
|
3
|
+
export async function localExists(library, type, id) {
|
|
4
4
|
try {
|
|
5
|
-
return Boolean(await recordGet(
|
|
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(
|
|
14
|
-
return recordCreate(
|
|
13
|
+
export function localPost(library, type, body) {
|
|
14
|
+
return recordCreate(library, type, body, 'import');
|
|
15
15
|
}
|
|
16
|
-
export function localPatch(
|
|
17
|
-
return recordUpdate(
|
|
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>;
|
package/dist/mcp-http.js
ADDED
|
@@ -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 };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import Fastify from 'fastify';
|
|
2
|
+
import { initDb, closeDb } from "./db.js";
|
|
3
|
+
import { systemEntities } from "./entity-schema.js";
|
|
4
|
+
import { registerAuthApi, resolveRequestUser, PUBLIC_API_PATHS } from "./auth-api.js";
|
|
5
|
+
import { registerMcpHttp } from "./mcp-http.js";
|
|
6
|
+
import { createUser, createApiToken } from "./auth-store.js";
|
|
7
|
+
import { pluginHooks } from "./plugin-hooks.js";
|
|
8
|
+
let currentAdminToken;
|
|
9
|
+
let currentMemberToken;
|
|
10
|
+
export async function freshMcpApp(opts = {}) {
|
|
11
|
+
process.env['DB_PATH'] = ':memory:';
|
|
12
|
+
const orm = await initDb(systemEntities);
|
|
13
|
+
await orm.schema.update({ safe: false, dropTables: false });
|
|
14
|
+
const app = Fastify();
|
|
15
|
+
app.addHook('onRequest', async (req, reply) => {
|
|
16
|
+
const gated = req.url.startsWith('/api/') || req.url.startsWith('/uploads/') || req.url === '/mcp' || req.url.startsWith('/mcp?');
|
|
17
|
+
if (!gated)
|
|
18
|
+
return;
|
|
19
|
+
if (PUBLIC_API_PATHS.some((p) => req.url.startsWith(p)))
|
|
20
|
+
return;
|
|
21
|
+
const user = await resolveRequestUser(req);
|
|
22
|
+
if (!user)
|
|
23
|
+
return reply.code(401).send({ error: 'unauthorized' });
|
|
24
|
+
req.user = user;
|
|
25
|
+
});
|
|
26
|
+
await registerAuthApi(app);
|
|
27
|
+
await registerMcpHttp(app);
|
|
28
|
+
const admin = await createUser({ login: 'admin', password: 'hunter2', displayName: 'Admin', role: 'admin' });
|
|
29
|
+
const member = await createUser({ login: 'member', password: 'hunter2', displayName: 'Member', role: 'member' });
|
|
30
|
+
currentAdminToken = `admin-raw-token-${Math.random().toString(36).slice(2)}`;
|
|
31
|
+
currentMemberToken = `member-raw-token-${Math.random().toString(36).slice(2)}`;
|
|
32
|
+
await createApiToken(admin.id, 'admin-token', currentAdminToken);
|
|
33
|
+
await createApiToken(member.id, 'member-token', currentMemberToken);
|
|
34
|
+
if (opts.withAdminTool || opts.withInstructions) {
|
|
35
|
+
pluginHooks['demo'] = {
|
|
36
|
+
agent: {
|
|
37
|
+
...(opts.withInstructions ? { instructions: 'Demo library — widgets.' } : {}),
|
|
38
|
+
...(opts.withAdminTool
|
|
39
|
+
? { tools: [{ name: 'danger', description: 'd', inputSchema: {}, role: 'admin', handler: () => 1 }] }
|
|
40
|
+
: {}),
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const origClose = app.close.bind(app);
|
|
45
|
+
app.close = ((...args) => {
|
|
46
|
+
delete pluginHooks['demo'];
|
|
47
|
+
return origClose(...args);
|
|
48
|
+
});
|
|
49
|
+
return app;
|
|
50
|
+
}
|
|
51
|
+
export async function adminToken() {
|
|
52
|
+
if (!currentAdminToken)
|
|
53
|
+
throw new Error('adminToken() called before freshMcpApp()');
|
|
54
|
+
return currentAdminToken;
|
|
55
|
+
}
|
|
56
|
+
export async function memberToken() {
|
|
57
|
+
if (!currentMemberToken)
|
|
58
|
+
throw new Error('memberToken() called before freshMcpApp()');
|
|
59
|
+
return currentMemberToken;
|
|
60
|
+
}
|
|
61
|
+
export function parseRpcResult(res) {
|
|
62
|
+
const contentType = String(res.headers['content-type'] ?? '');
|
|
63
|
+
const raw = [];
|
|
64
|
+
if (contentType.includes('text/event-stream')) {
|
|
65
|
+
for (const line of res.body.split('\n')) {
|
|
66
|
+
const trimmed = line.trim();
|
|
67
|
+
if (trimmed.startsWith('data:'))
|
|
68
|
+
raw.push(trimmed.slice('data:'.length).trim());
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
raw.push(res.body);
|
|
73
|
+
}
|
|
74
|
+
for (const s of raw) {
|
|
75
|
+
if (!s)
|
|
76
|
+
continue;
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(s);
|
|
79
|
+
const msg = (Array.isArray(parsed) ? parsed : [parsed]).find((m) => m.result);
|
|
80
|
+
if (msg?.result)
|
|
81
|
+
return msg.result;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
export function parseToolNames(res) {
|
|
89
|
+
const contentType = String(res.headers['content-type'] ?? '');
|
|
90
|
+
const messages = [];
|
|
91
|
+
if (contentType.includes('text/event-stream')) {
|
|
92
|
+
for (const line of res.body.split('\n')) {
|
|
93
|
+
const trimmed = line.trim();
|
|
94
|
+
if (!trimmed.startsWith('data:'))
|
|
95
|
+
continue;
|
|
96
|
+
const jsonStr = trimmed.slice('data:'.length).trim();
|
|
97
|
+
if (!jsonStr)
|
|
98
|
+
continue;
|
|
99
|
+
try {
|
|
100
|
+
messages.push(JSON.parse(jsonStr));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(res.body);
|
|
109
|
+
messages.push(...(Array.isArray(parsed) ? parsed : [parsed]));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const withTools = messages.find((m) => m.result?.tools);
|
|
115
|
+
return (withTools?.result?.tools ?? []).map((t) => t.name);
|
|
116
|
+
}
|
|
117
|
+
export { closeDb };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CofferClientApi } from '@coffer-org/mcp/client';
|
|
2
|
+
export declare function mapError(e: unknown): never;
|
|
3
|
+
export declare class LocalClient implements CofferClientApi {
|
|
4
|
+
getSchema(): Promise<unknown>;
|
|
5
|
+
listRecords(library: string, type: string, query?: Record<string, unknown>): Promise<unknown>;
|
|
6
|
+
getRecord(library: string, type: string, id: number): Promise<unknown>;
|
|
7
|
+
createRecord(library: string, type: string, fields: Record<string, unknown>): Promise<unknown>;
|
|
8
|
+
updateRecord(library: string, type: string, id: number, fields: Record<string, unknown>): Promise<unknown>;
|
|
9
|
+
deleteRecord(library: string, type: string, id: number): Promise<unknown>;
|
|
10
|
+
}
|