@redaksjon/protokoll 1.0.21 ā 1.0.22-dev.20260227214704.08e3d54
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/.dockerignore +42 -0
- package/.gcloudignore +43 -0
- package/deploy/cloud-run/Dockerfile +35 -0
- package/deploy/cloud-run/env.example.yaml +47 -0
- package/dist/configDiscovery.js +999 -40
- package/dist/configDiscovery.js.map +1 -1
- package/dist/mcp/server-hono.js +372 -121
- package/dist/mcp/server-hono.js.map +1 -1
- package/dist/mcp/server.js +4 -2
- package/dist/mcp/server.js.map +1 -1
- package/docs/gcs-authentication.md +70 -0
- package/docs/mcp-session-recovery.md +30 -0
- package/docs/storage-backends.md +79 -0
- package/guide/cloud-run.md +150 -0
- package/guide/index.md +1 -0
- package/package.json +4 -1
- package/scripts/ensure-executable-bins.mjs +29 -0
package/dist/mcp/server-hono.js
CHANGED
|
@@ -8,26 +8,38 @@ import { StreamableHTTPTransport } from '@hono/mcp';
|
|
|
8
8
|
import { cors } from 'hono/cors';
|
|
9
9
|
import { streamSSE } from 'hono/streaming';
|
|
10
10
|
import { bodyLimit } from 'hono/body-limit';
|
|
11
|
-
import { createReadStream } from 'node:fs';
|
|
12
|
-
import * as fs from 'node:fs/promises';
|
|
13
11
|
import { join, isAbsolute, extname, basename, resolve, dirname } from 'node:path';
|
|
14
12
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
15
13
|
import { ListRootsRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
16
14
|
import * as Cardigantime from '@utilarium/cardigantime';
|
|
15
|
+
import Logging from '@fjell/logging';
|
|
17
16
|
import { z } from 'zod';
|
|
18
|
-
import { f as getOutputDirectory, D as DEFAULT_CONFIG_FILE, s as setRoots, d as initializeServerConfig,
|
|
17
|
+
import { f as getOutputDirectory, j as getOutputStorage, D as DEFAULT_CONFIG_FILE, s as setRoots, d as initializeServerConfig, k as getServerConfig, l as getStorageConfig, m as getContext, n as createQuietLogger, t as tools, h as handleToolCall, a as handleListResources, b as handleReadResource, g as getPrompts, c as getPrompt, e as getCachedRoots, o as setWorkerInstance } from '../configDiscovery.js';
|
|
19
18
|
import { tmpdir } from 'node:os';
|
|
19
|
+
import * as fs from 'node:fs/promises';
|
|
20
20
|
import { glob } from 'glob';
|
|
21
21
|
import { Transcript, Weighting, Pipeline } from '@redaksjon/protokoll-engine';
|
|
22
22
|
import { PklTranscript } from '@redaksjon/protokoll-format';
|
|
23
23
|
import 'js-yaml';
|
|
24
|
+
import 'node:fs';
|
|
24
25
|
import 'node:url';
|
|
25
26
|
import '@redaksjon/context';
|
|
26
27
|
import 'winston';
|
|
27
28
|
import 'crypto';
|
|
29
|
+
import '@google-cloud/storage';
|
|
28
30
|
|
|
29
31
|
const { findUploadedTranscripts, markTranscriptAsTranscribing, markTranscriptAsFailed } = Transcript;
|
|
30
32
|
const WEIGHT_MODEL_FILENAME = ".protokoll-weight-model.json";
|
|
33
|
+
async function materializeUploadedAudio(outputStorage, uploadPath) {
|
|
34
|
+
const fileName = uploadPath.split("/").pop() || "uploaded-audio.bin";
|
|
35
|
+
const tempPath = join(
|
|
36
|
+
tmpdir(),
|
|
37
|
+
`protokoll-worker-${Date.now()}-${Math.random().toString(36).slice(2)}-${fileName}`
|
|
38
|
+
);
|
|
39
|
+
const contents = await outputStorage.readFile(uploadPath);
|
|
40
|
+
await fs.writeFile(tempPath, contents);
|
|
41
|
+
return tempPath;
|
|
42
|
+
}
|
|
31
43
|
class TranscriptionWorker {
|
|
32
44
|
isRunning = false;
|
|
33
45
|
config;
|
|
@@ -150,6 +162,7 @@ class TranscriptionWorker {
|
|
|
150
162
|
*/
|
|
151
163
|
async processNextTranscript(item) {
|
|
152
164
|
this.stats.currentTask = `Processing ${item.uuid}`;
|
|
165
|
+
let localTempAudioPath = null;
|
|
153
166
|
console.log(`
|
|
154
167
|
š Processing transcript: ${item.uuid}`);
|
|
155
168
|
console.log(` Audio file: ${item.metadata.audioFile}`);
|
|
@@ -157,15 +170,35 @@ class TranscriptionWorker {
|
|
|
157
170
|
try {
|
|
158
171
|
await markTranscriptAsTranscribing(item.filePath);
|
|
159
172
|
let audioFilePath = item.metadata.audioFile && isAbsolute(item.metadata.audioFile) ? item.metadata.audioFile : join(this.config.uploadDirectory, item.metadata.audioFile || "");
|
|
173
|
+
const outputStorage = this.config.outputStorage;
|
|
174
|
+
const isGcsOutput = outputStorage?.name === "gcs";
|
|
175
|
+
if (isGcsOutput && item.metadata.audioFile && !isAbsolute(item.metadata.audioFile)) {
|
|
176
|
+
const uploadObjectPath = join("uploads", item.metadata.audioFile).replace(/\\/g, "/");
|
|
177
|
+
localTempAudioPath = await materializeUploadedAudio(outputStorage, uploadObjectPath);
|
|
178
|
+
audioFilePath = localTempAudioPath;
|
|
179
|
+
}
|
|
160
180
|
try {
|
|
161
181
|
await fs.stat(audioFilePath);
|
|
162
182
|
} catch {
|
|
163
183
|
if (item.metadata.audioHash) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
184
|
+
if (isGcsOutput && outputStorage) {
|
|
185
|
+
const uploadMatches = await outputStorage.listFiles("uploads", item.metadata.audioHash);
|
|
186
|
+
const byHash = uploadMatches.filter(
|
|
187
|
+
(candidate) => candidate.split("/").pop()?.startsWith(`${item.metadata.audioHash}.`)
|
|
188
|
+
);
|
|
189
|
+
if (byHash.length > 0) {
|
|
190
|
+
localTempAudioPath = await materializeUploadedAudio(outputStorage, byHash[0]);
|
|
191
|
+
audioFilePath = localTempAudioPath;
|
|
192
|
+
} else {
|
|
193
|
+
throw new Error(`Audio file not found and no object matching hash ${item.metadata.audioHash} in uploads`);
|
|
194
|
+
}
|
|
167
195
|
} else {
|
|
168
|
-
|
|
196
|
+
const byHash = await glob(`${item.metadata.audioHash}.*`, { cwd: this.config.uploadDirectory, absolute: true });
|
|
197
|
+
if (byHash.length > 0) {
|
|
198
|
+
audioFilePath = byHash[0];
|
|
199
|
+
} else {
|
|
200
|
+
throw new Error(`Audio file not found at ${audioFilePath} and no file matching hash ${item.metadata.audioHash} in uploads`);
|
|
201
|
+
}
|
|
169
202
|
}
|
|
170
203
|
} else {
|
|
171
204
|
throw new Error(`Audio file not found at ${audioFilePath} and no audioHash for fallback lookup`);
|
|
@@ -278,6 +311,10 @@ class TranscriptionWorker {
|
|
|
278
311
|
console.error(`ā Failed to process ${item.uuid}:`, errorMessage);
|
|
279
312
|
await markTranscriptAsFailed(item.filePath, errorMessage);
|
|
280
313
|
this.stats.currentTask = void 0;
|
|
314
|
+
} finally {
|
|
315
|
+
if (localTempAudioPath) {
|
|
316
|
+
await fs.rm(localTempAudioPath, { force: true });
|
|
317
|
+
}
|
|
281
318
|
}
|
|
282
319
|
}
|
|
283
320
|
/**
|
|
@@ -319,6 +356,19 @@ class TranscriptionWorker {
|
|
|
319
356
|
}
|
|
320
357
|
|
|
321
358
|
const { createUploadTranscript, findTranscriptByUuid } = Transcript;
|
|
359
|
+
function bootstrapHttpLogLevel() {
|
|
360
|
+
const debugFromCli = process.argv.includes("--debug");
|
|
361
|
+
const debugFromEnv = parseBooleanEnv(process.env.PROTOKOLL_DEBUG) === true;
|
|
362
|
+
configureHttpLogLevel(debugFromCli || debugFromEnv);
|
|
363
|
+
}
|
|
364
|
+
bootstrapHttpLogLevel();
|
|
365
|
+
const rootLogger = Logging.getLogger("@redaksjon/protokoll-mcp").get("http");
|
|
366
|
+
const sessionLogger = rootLogger.get("session");
|
|
367
|
+
const requestLogger = rootLogger.get("request");
|
|
368
|
+
const transportLogger = rootLogger.get("transport");
|
|
369
|
+
const sseLogger = rootLogger.get("sse");
|
|
370
|
+
const lifecycleLogger = rootLogger.get("lifecycle");
|
|
371
|
+
const uploadLogger = rootLogger.get("upload");
|
|
322
372
|
let startupConfig = {};
|
|
323
373
|
const DEFAULT_MAX_AUDIO_SIZE = 1024 * 1024 * 1024;
|
|
324
374
|
const DEFAULT_AUDIO_EXTENSIONS = ["mp3", "m4a", "wav", "webm", "mp4", "aac", "ogg", "flac"];
|
|
@@ -326,6 +376,97 @@ function parseBooleanEnv(value) {
|
|
|
326
376
|
if (!value) return void 0;
|
|
327
377
|
return value.toLowerCase() === "true";
|
|
328
378
|
}
|
|
379
|
+
function readNonEmptyString(value) {
|
|
380
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
381
|
+
}
|
|
382
|
+
function readEnvString(name) {
|
|
383
|
+
return readNonEmptyString(process.env[name]);
|
|
384
|
+
}
|
|
385
|
+
function buildEnvStorageConfig() {
|
|
386
|
+
const configuredBackend = readEnvString("PROTOKOLL_STORAGE_BACKEND");
|
|
387
|
+
const gcs = {
|
|
388
|
+
projectId: readEnvString("PROTOKOLL_STORAGE_GCS_PROJECT_ID"),
|
|
389
|
+
inputUri: readEnvString("PROTOKOLL_STORAGE_GCS_INPUT_URI"),
|
|
390
|
+
outputUri: readEnvString("PROTOKOLL_STORAGE_GCS_OUTPUT_URI"),
|
|
391
|
+
contextUri: readEnvString("PROTOKOLL_STORAGE_GCS_CONTEXT_URI"),
|
|
392
|
+
inputBucket: readEnvString("PROTOKOLL_STORAGE_GCS_INPUT_BUCKET"),
|
|
393
|
+
inputPrefix: readEnvString("PROTOKOLL_STORAGE_GCS_INPUT_PREFIX"),
|
|
394
|
+
outputBucket: readEnvString("PROTOKOLL_STORAGE_GCS_OUTPUT_BUCKET"),
|
|
395
|
+
outputPrefix: readEnvString("PROTOKOLL_STORAGE_GCS_OUTPUT_PREFIX"),
|
|
396
|
+
contextBucket: readEnvString("PROTOKOLL_STORAGE_GCS_CONTEXT_BUCKET"),
|
|
397
|
+
contextPrefix: readEnvString("PROTOKOLL_STORAGE_GCS_CONTEXT_PREFIX"),
|
|
398
|
+
credentialsFile: readEnvString("PROTOKOLL_STORAGE_GCS_CREDENTIALS_FILE")
|
|
399
|
+
};
|
|
400
|
+
const hasGcsValue = Object.values(gcs).some((value) => value !== void 0);
|
|
401
|
+
const backend = configuredBackend ?? (hasGcsValue ? "gcs" : void 0);
|
|
402
|
+
if (!backend) return void 0;
|
|
403
|
+
const storage = {};
|
|
404
|
+
if (backend) storage.backend = backend;
|
|
405
|
+
if (hasGcsValue) storage.gcs = gcs;
|
|
406
|
+
return storage;
|
|
407
|
+
}
|
|
408
|
+
function describeRawStorageConfig(config) {
|
|
409
|
+
const lines = [];
|
|
410
|
+
const rawStorage = config.storage;
|
|
411
|
+
if (!rawStorage || typeof rawStorage !== "object") {
|
|
412
|
+
lines.push("šļø Storage Backend: filesystem (default)");
|
|
413
|
+
return lines;
|
|
414
|
+
}
|
|
415
|
+
const storage = rawStorage;
|
|
416
|
+
const backend = storage.backend === "gcs" ? "gcs" : "filesystem";
|
|
417
|
+
lines.push(`šļø Storage Backend: ${backend}`);
|
|
418
|
+
if (backend !== "gcs") {
|
|
419
|
+
return lines;
|
|
420
|
+
}
|
|
421
|
+
const gcs = typeof storage.gcs === "object" && storage.gcs !== null ? storage.gcs : {};
|
|
422
|
+
const projectId = readNonEmptyString(gcs.projectId);
|
|
423
|
+
const inputBucket = readNonEmptyString(gcs.inputBucket);
|
|
424
|
+
const inputPrefix = readNonEmptyString(gcs.inputPrefix) ?? "";
|
|
425
|
+
const outputBucket = readNonEmptyString(gcs.outputBucket);
|
|
426
|
+
const outputPrefix = readNonEmptyString(gcs.outputPrefix) ?? "";
|
|
427
|
+
const contextBucket = readNonEmptyString(gcs.contextBucket);
|
|
428
|
+
const contextPrefix = readNonEmptyString(gcs.contextPrefix) ?? "";
|
|
429
|
+
const inputUri = readNonEmptyString(gcs.inputUri);
|
|
430
|
+
const outputUri = readNonEmptyString(gcs.outputUri);
|
|
431
|
+
const contextUri = readNonEmptyString(gcs.contextUri);
|
|
432
|
+
const credentialsFile = readNonEmptyString(gcs.credentialsFile);
|
|
433
|
+
if (projectId) lines.push(`āļø GCP Project: ${projectId}`);
|
|
434
|
+
if (inputBucket) lines.push(`š„ GCS Input: ${inputBucket}/${inputPrefix}`);
|
|
435
|
+
else if (inputUri) lines.push(`š„ GCS Input URI: ${inputUri}`);
|
|
436
|
+
if (outputBucket) lines.push(`š¤ GCS Output: ${outputBucket}/${outputPrefix}`);
|
|
437
|
+
else if (outputUri) lines.push(`š¤ GCS Output URI: ${outputUri}`);
|
|
438
|
+
if (contextBucket) lines.push(`š GCS Context: ${contextBucket}/${contextPrefix}`);
|
|
439
|
+
else if (contextUri) lines.push(`š GCS Context URI: ${contextUri}`);
|
|
440
|
+
lines.push(`š GCS Credentials: ${credentialsFile ?? "ADC/default environment"}`);
|
|
441
|
+
return lines;
|
|
442
|
+
}
|
|
443
|
+
function configureHttpLogLevel(debug) {
|
|
444
|
+
const packageName = "@redaksjon/protokoll-mcp";
|
|
445
|
+
const logLevel = debug ? "DEBUG" : "INFO";
|
|
446
|
+
let parsed = {};
|
|
447
|
+
const raw = process.env.LOGGING_CONFIG;
|
|
448
|
+
if (raw) {
|
|
449
|
+
try {
|
|
450
|
+
parsed = JSON.parse(raw);
|
|
451
|
+
} catch {
|
|
452
|
+
parsed = {};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const overrides = parsed.overrides || {};
|
|
456
|
+
const packageOverride = overrides[packageName] || {};
|
|
457
|
+
process.env.LOGGING_CONFIG = JSON.stringify({
|
|
458
|
+
logLevel: parsed.logLevel || "INFO",
|
|
459
|
+
logFormat: parsed.logFormat || "TEXT",
|
|
460
|
+
...parsed,
|
|
461
|
+
overrides: {
|
|
462
|
+
...overrides,
|
|
463
|
+
[packageName]: {
|
|
464
|
+
...packageOverride,
|
|
465
|
+
logLevel
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}
|
|
329
470
|
function parseCsvList(value) {
|
|
330
471
|
return value.split(",").map((part) => part.trim()).filter(Boolean);
|
|
331
472
|
}
|
|
@@ -368,7 +509,10 @@ data: ${notification}
|
|
|
368
509
|
}
|
|
369
510
|
}
|
|
370
511
|
}
|
|
371
|
-
|
|
512
|
+
rootLogger.info("entity.notification.sent", {
|
|
513
|
+
entityUri,
|
|
514
|
+
notifiedWriters: notified
|
|
515
|
+
});
|
|
372
516
|
}
|
|
373
517
|
async function sendEntityChangeNotifications(toolName, args) {
|
|
374
518
|
if (!args) return;
|
|
@@ -388,7 +532,10 @@ async function sendEntityChangeNotifications(toolName, args) {
|
|
|
388
532
|
await notifyEntityChanged(ref.entityType, ref.entityId);
|
|
389
533
|
}
|
|
390
534
|
} catch (err) {
|
|
391
|
-
|
|
535
|
+
rootLogger.error("entity.notification.error", {
|
|
536
|
+
toolName,
|
|
537
|
+
error: err instanceof Error ? err.message : String(err)
|
|
538
|
+
});
|
|
392
539
|
}
|
|
393
540
|
}
|
|
394
541
|
const SESSION_TIMEOUT = 60 * 60 * 1e3;
|
|
@@ -396,7 +543,7 @@ setInterval(() => {
|
|
|
396
543
|
const now = Date.now();
|
|
397
544
|
for (const [sessionId, session] of sessions.entries()) {
|
|
398
545
|
if (now - session.lastActivity > SESSION_TIMEOUT) {
|
|
399
|
-
|
|
546
|
+
sessionLogger.info("cleanup.inactive_session", { sessionId });
|
|
400
547
|
sessions.delete(sessionId);
|
|
401
548
|
}
|
|
402
549
|
}
|
|
@@ -433,7 +580,10 @@ function createMcpServer() {
|
|
|
433
580
|
try {
|
|
434
581
|
const result = await handleToolCall(name, args);
|
|
435
582
|
sendEntityChangeNotifications(name, args).catch((err) => {
|
|
436
|
-
|
|
583
|
+
rootLogger.error("entity.notification.dispatch_error", {
|
|
584
|
+
toolName: name,
|
|
585
|
+
error: err instanceof Error ? err.message : String(err)
|
|
586
|
+
});
|
|
437
587
|
});
|
|
438
588
|
return {
|
|
439
589
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
@@ -515,18 +665,18 @@ app.post(
|
|
|
515
665
|
}, 400);
|
|
516
666
|
}
|
|
517
667
|
const outputDir = getOutputDirectory();
|
|
518
|
-
const
|
|
519
|
-
await
|
|
668
|
+
const outputStorage = getOutputStorage();
|
|
669
|
+
await outputStorage.mkdir("uploads");
|
|
520
670
|
const buffer = await file.arrayBuffer();
|
|
521
671
|
const hash = createHash("sha256").update(Buffer.from(buffer)).digest("hex");
|
|
522
|
-
const
|
|
523
|
-
await
|
|
672
|
+
const uploadObjectPath = `uploads/${hash}.${ext}`;
|
|
673
|
+
await outputStorage.writeFile(uploadObjectPath, Buffer.from(buffer));
|
|
524
674
|
const rawTitle = body["title"];
|
|
525
675
|
const rawProject = body["project"];
|
|
526
676
|
const title = typeof rawTitle === "string" && rawTitle.trim() ? rawTitle.trim() : void 0;
|
|
527
677
|
const project = typeof rawProject === "string" && rawProject.trim() ? rawProject.trim() : void 0;
|
|
528
678
|
const { uuid } = await createUploadTranscript({
|
|
529
|
-
audioFile: basename(
|
|
679
|
+
audioFile: basename(uploadObjectPath),
|
|
530
680
|
// Store just the filename
|
|
531
681
|
originalFilename: file.name,
|
|
532
682
|
audioHash: hash,
|
|
@@ -534,8 +684,13 @@ app.post(
|
|
|
534
684
|
title,
|
|
535
685
|
project
|
|
536
686
|
});
|
|
537
|
-
|
|
538
|
-
|
|
687
|
+
uploadLogger.info("audio.upload.complete", {
|
|
688
|
+
originalFilename: file.name,
|
|
689
|
+
transcriptUuid: uuid,
|
|
690
|
+
bytes: buffer.byteLength,
|
|
691
|
+
objectPath: uploadObjectPath,
|
|
692
|
+
storageBackend: outputStorage.name
|
|
693
|
+
});
|
|
539
694
|
return c.json({
|
|
540
695
|
success: true,
|
|
541
696
|
uuid,
|
|
@@ -546,7 +701,9 @@ app.post(
|
|
|
546
701
|
project: project ?? null
|
|
547
702
|
});
|
|
548
703
|
} catch (error) {
|
|
549
|
-
|
|
704
|
+
uploadLogger.error("audio.upload.error", {
|
|
705
|
+
error: error instanceof Error ? error.message : String(error)
|
|
706
|
+
});
|
|
550
707
|
return c.json({
|
|
551
708
|
error: "Upload failed",
|
|
552
709
|
details: error instanceof Error ? error.message : String(error)
|
|
@@ -558,7 +715,7 @@ app.get("/audio/:uuid", async (c) => {
|
|
|
558
715
|
try {
|
|
559
716
|
const uuid = c.req.param("uuid");
|
|
560
717
|
const outputDir = getOutputDirectory();
|
|
561
|
-
const
|
|
718
|
+
const outputStorage = getOutputStorage();
|
|
562
719
|
const filePath = await findTranscriptByUuid(uuid, [outputDir]);
|
|
563
720
|
if (!filePath) {
|
|
564
721
|
return c.json({ error: `Transcript not found for UUID: ${uuid}` }, 404);
|
|
@@ -569,20 +726,23 @@ app.get("/audio/:uuid", async (c) => {
|
|
|
569
726
|
if (!metadata.audioHash) {
|
|
570
727
|
return c.json({ error: "No audio file associated with this transcript" }, 404);
|
|
571
728
|
}
|
|
572
|
-
const
|
|
729
|
+
const uploadedFiles = await outputStorage.listFiles("uploads", metadata.audioHash);
|
|
730
|
+
const audioFiles = uploadedFiles.filter((filePath2) => basename(filePath2).startsWith(`${metadata.audioHash}.`));
|
|
573
731
|
if (audioFiles.length === 0) {
|
|
574
732
|
return c.json({ error: "Audio file not found in uploads directory" }, 404);
|
|
575
733
|
}
|
|
576
734
|
const audioFile = audioFiles[0];
|
|
577
735
|
const ext = extname(audioFile);
|
|
578
|
-
const
|
|
736
|
+
const audioBuffer = await outputStorage.readFile(audioFile);
|
|
579
737
|
c.header("Content-Type", getAudioMimeType(ext));
|
|
580
|
-
c.header("Content-Length",
|
|
738
|
+
c.header("Content-Length", audioBuffer.length.toString());
|
|
581
739
|
c.header("Content-Disposition", `attachment; filename="${metadata.audioFile || `${uuid}${ext}`}"`);
|
|
582
|
-
|
|
583
|
-
return c.body(stream);
|
|
740
|
+
return c.body(audioBuffer);
|
|
584
741
|
} catch (error) {
|
|
585
|
-
|
|
742
|
+
uploadLogger.error("audio.download.error", {
|
|
743
|
+
uuid: c.req.param("uuid"),
|
|
744
|
+
error: error instanceof Error ? error.message : String(error)
|
|
745
|
+
});
|
|
586
746
|
return c.json({
|
|
587
747
|
error: "Download failed",
|
|
588
748
|
details: error instanceof Error ? error.message : String(error)
|
|
@@ -621,11 +781,20 @@ app.get("/health", async (c) => {
|
|
|
621
781
|
}
|
|
622
782
|
});
|
|
623
783
|
app.post("/mcp", async (c) => {
|
|
784
|
+
const requestStartedAt = Date.now();
|
|
624
785
|
const sessionIdHeader = c.req.header("mcp-session-id");
|
|
625
786
|
const body = await c.req.text();
|
|
626
787
|
let jsonRpcMessage;
|
|
627
788
|
try {
|
|
628
789
|
jsonRpcMessage = JSON.parse(body);
|
|
790
|
+
requestLogger.debug("request.parse_jsonrpc", {
|
|
791
|
+
method: "POST",
|
|
792
|
+
route: "/mcp",
|
|
793
|
+
sessionId: sessionIdHeader ?? null,
|
|
794
|
+
rpcMethod: jsonRpcMessage.method ?? null,
|
|
795
|
+
rpcId: jsonRpcMessage.id ?? null,
|
|
796
|
+
elapsedMs: Date.now() - requestStartedAt
|
|
797
|
+
});
|
|
629
798
|
} catch {
|
|
630
799
|
return c.json({
|
|
631
800
|
jsonrpc: "2.0",
|
|
@@ -634,20 +803,11 @@ app.post("/mcp", async (c) => {
|
|
|
634
803
|
}, 400);
|
|
635
804
|
}
|
|
636
805
|
const isInitialize = jsonRpcMessage.method === "initialize";
|
|
637
|
-
|
|
638
|
-
return c.json({
|
|
639
|
-
jsonrpc: "2.0",
|
|
640
|
-
error: { code: -32e3, message: "Missing Mcp-Session-Id header" },
|
|
641
|
-
id: jsonRpcMessage.id || null
|
|
642
|
-
}, 400);
|
|
643
|
-
}
|
|
644
|
-
let session;
|
|
645
|
-
if (isInitialize) {
|
|
646
|
-
const newSessionId = randomUUID();
|
|
806
|
+
const createSession = async (sessionId) => {
|
|
647
807
|
const server = createMcpServer();
|
|
648
808
|
const transport = new StreamableHTTPTransport();
|
|
649
|
-
|
|
650
|
-
sessionId
|
|
809
|
+
const newSession = {
|
|
810
|
+
sessionId,
|
|
651
811
|
server,
|
|
652
812
|
transport,
|
|
653
813
|
initialized: false,
|
|
@@ -655,105 +815,187 @@ app.post("/mcp", async (c) => {
|
|
|
655
815
|
subscriptions: /* @__PURE__ */ new Set(),
|
|
656
816
|
sseWriters: /* @__PURE__ */ new Set()
|
|
657
817
|
};
|
|
658
|
-
sessions.set(
|
|
818
|
+
sessions.set(sessionId, newSession);
|
|
819
|
+
sessionLogger.info("created", {
|
|
820
|
+
sessionId,
|
|
821
|
+
workspaceRoot: process.env.WORKSPACE_ROOT || process.cwd()
|
|
822
|
+
});
|
|
659
823
|
const cardigantimeConfig = startupConfig;
|
|
660
824
|
const resolvedConfigDirs = cardigantimeConfig.resolvedConfigDirs;
|
|
661
825
|
const configRoot = Array.isArray(resolvedConfigDirs) && resolvedConfigDirs.length > 0 ? resolvedConfigDirs[0] : process.env.WORKSPACE_ROOT || process.cwd();
|
|
662
826
|
const configPathDisplay = resolve(configRoot, DEFAULT_CONFIG_FILE);
|
|
663
|
-
const
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
827
|
+
const startupResolvedConfigDirs = startupConfig.resolvedConfigDirs;
|
|
828
|
+
const rootCandidates = Array.isArray(startupResolvedConfigDirs) && startupResolvedConfigDirs.length > 0 ? startupResolvedConfigDirs : [process.env.WORKSPACE_ROOT || process.cwd()];
|
|
829
|
+
const workspaceRoot = rootCandidates[0];
|
|
830
|
+
const initialRoots = rootCandidates.map((rootPath, index) => ({
|
|
831
|
+
uri: `file://${rootPath}`,
|
|
832
|
+
name: index === 0 ? "Workspace" : `Workspace ${index + 1}`
|
|
833
|
+
}));
|
|
834
|
+
process.env.WORKSPACE_ROOT = workspaceRoot;
|
|
668
835
|
setRoots(initialRoots);
|
|
669
836
|
await initializeServerConfig(initialRoots, "remote");
|
|
670
837
|
const serverConfig = getServerConfig();
|
|
838
|
+
const storageConfig = getStorageConfig();
|
|
671
839
|
const context = getContext();
|
|
672
840
|
const contextDirs = context?.getContextDirs?.() ?? serverConfig.configFile?.contextDirectories;
|
|
673
841
|
const contextDirsDisplay = Array.isArray(contextDirs) && contextDirs.length > 0 ? contextDirs.join(", ") : "NONE";
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
842
|
+
sessionLogger.info("configuration.loaded", {
|
|
843
|
+
sessionId,
|
|
844
|
+
workingDirectory: workspaceRoot,
|
|
845
|
+
configFile: configPathDisplay,
|
|
846
|
+
inputDirectory: serverConfig.inputDirectory || null,
|
|
847
|
+
outputDirectory: serverConfig.outputDirectory || null,
|
|
848
|
+
processedDirectory: serverConfig.processedDirectory || null,
|
|
849
|
+
contextDirectories: contextDirsDisplay,
|
|
850
|
+
storageBackend: storageConfig.backend,
|
|
851
|
+
model: cardigantimeConfig.model || "default",
|
|
852
|
+
transcriptionModel: cardigantimeConfig.transcriptionModel || "default",
|
|
853
|
+
debugMode: cardigantimeConfig.debug ? "ON" : "OFF",
|
|
854
|
+
verboseMode: cardigantimeConfig.verbose ? "ON" : "OFF"
|
|
855
|
+
});
|
|
856
|
+
if (storageConfig.backend === "gcs" && storageConfig.gcs) {
|
|
857
|
+
sessionLogger.info("configuration.gcs", {
|
|
858
|
+
sessionId,
|
|
859
|
+
projectId: storageConfig.gcs.projectId ?? process.env.GOOGLE_CLOUD_PROJECT ?? "default",
|
|
860
|
+
credentialsFile: storageConfig.gcs.credentialsFile ?? "ADC/default environment",
|
|
861
|
+
inputBucket: storageConfig.gcs.inputBucket ?? null,
|
|
862
|
+
inputPrefix: storageConfig.gcs.inputPrefix ?? null,
|
|
863
|
+
outputBucket: storageConfig.gcs.outputBucket ?? null,
|
|
864
|
+
outputPrefix: storageConfig.gcs.outputPrefix ?? null,
|
|
865
|
+
contextBucket: storageConfig.gcs.contextBucket ?? null,
|
|
866
|
+
contextPrefix: storageConfig.gcs.contextPrefix ?? null
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
await newSession.server.connect(transport);
|
|
870
|
+
return newSession;
|
|
871
|
+
};
|
|
872
|
+
let session;
|
|
873
|
+
if (isInitialize) {
|
|
874
|
+
const newSessionId = randomUUID();
|
|
875
|
+
session = await createSession(newSessionId);
|
|
693
876
|
} else {
|
|
694
|
-
|
|
877
|
+
const requestedSessionId = sessionIdHeader?.trim() || randomUUID();
|
|
878
|
+
session = sessions.get(requestedSessionId);
|
|
695
879
|
if (!session) {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
}
|
|
880
|
+
sessionLogger.warning("recovered.missing_session", {
|
|
881
|
+
requestedSessionId: sessionIdHeader ?? null,
|
|
882
|
+
rpcMethod: jsonRpcMessage.method ?? null,
|
|
883
|
+
rpcId: jsonRpcMessage.id ?? null
|
|
884
|
+
});
|
|
885
|
+
session = await createSession(requestedSessionId);
|
|
701
886
|
}
|
|
702
887
|
session.lastActivity = Date.now();
|
|
888
|
+
sessionLogger.debug("reused", { sessionId: session.sessionId });
|
|
703
889
|
}
|
|
890
|
+
c.header("Mcp-Session-Id", session.sessionId);
|
|
704
891
|
const isNotification = jsonRpcMessage.id === void 0 || jsonRpcMessage.id === null;
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
892
|
+
requestLogger.info("incoming", {
|
|
893
|
+
method: "POST",
|
|
894
|
+
route: "/mcp",
|
|
895
|
+
sessionId: session.sessionId,
|
|
896
|
+
rpcMethod: jsonRpcMessage.method ?? null,
|
|
897
|
+
rpcId: jsonRpcMessage.id ?? null,
|
|
898
|
+
rpcType: isNotification ? "notification" : "request"
|
|
899
|
+
});
|
|
900
|
+
requestLogger.debug("incoming.params", {
|
|
901
|
+
sessionId: session.sessionId,
|
|
902
|
+
rpcMethod: jsonRpcMessage.method ?? null,
|
|
903
|
+
rpcId: jsonRpcMessage.id ?? null,
|
|
904
|
+
params: jsonRpcMessage.params ?? null
|
|
905
|
+
});
|
|
718
906
|
if (isNotification) {
|
|
719
907
|
switch (jsonRpcMessage.method) {
|
|
720
908
|
case "notifications/initialized":
|
|
721
909
|
session.initialized = true;
|
|
722
|
-
|
|
910
|
+
requestLogger.info("notification.initialized", {
|
|
911
|
+
sessionId: session.sessionId,
|
|
912
|
+
rpcMethod: jsonRpcMessage.method
|
|
913
|
+
});
|
|
723
914
|
break;
|
|
724
915
|
case "notifications/cancelled":
|
|
725
|
-
|
|
916
|
+
requestLogger.info("notification.cancelled", {
|
|
917
|
+
sessionId: session.sessionId,
|
|
918
|
+
params: jsonRpcMessage.params ?? null
|
|
919
|
+
});
|
|
726
920
|
break;
|
|
727
921
|
default:
|
|
728
|
-
|
|
922
|
+
requestLogger.debug("notification.unknown", {
|
|
923
|
+
sessionId: session.sessionId,
|
|
924
|
+
rpcMethod: jsonRpcMessage.method
|
|
925
|
+
});
|
|
729
926
|
}
|
|
730
|
-
|
|
731
|
-
|
|
927
|
+
requestLogger.info("complete", {
|
|
928
|
+
method: "POST",
|
|
929
|
+
route: "/mcp",
|
|
930
|
+
sessionId: session.sessionId,
|
|
931
|
+
rpcMethod: jsonRpcMessage.method ?? null,
|
|
932
|
+
rpcId: jsonRpcMessage.id ?? null,
|
|
933
|
+
status: 202,
|
|
934
|
+
elapsedMs: Date.now() - requestStartedAt
|
|
935
|
+
});
|
|
732
936
|
return c.body(null, 202);
|
|
733
937
|
}
|
|
734
938
|
if (jsonRpcMessage.method === "resources/subscribe") {
|
|
735
939
|
const uri = jsonRpcMessage.params?.uri;
|
|
736
940
|
if (uri) {
|
|
737
941
|
session.subscriptions.add(uri);
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
942
|
+
sessionLogger.info("subscription.created", {
|
|
943
|
+
sessionId: session.sessionId,
|
|
944
|
+
uri,
|
|
945
|
+
totalSubscriptions: session.subscriptions.size
|
|
946
|
+
});
|
|
742
947
|
}
|
|
948
|
+
requestLogger.info("complete", {
|
|
949
|
+
method: "POST",
|
|
950
|
+
route: "/mcp",
|
|
951
|
+
sessionId: session.sessionId,
|
|
952
|
+
rpcMethod: jsonRpcMessage.method,
|
|
953
|
+
rpcId: jsonRpcMessage.id ?? null,
|
|
954
|
+
status: 200,
|
|
955
|
+
elapsedMs: Date.now() - requestStartedAt
|
|
956
|
+
});
|
|
743
957
|
return c.json({ jsonrpc: "2.0", result: {}, id: jsonRpcMessage.id });
|
|
744
958
|
}
|
|
745
959
|
if (jsonRpcMessage.method === "resources/unsubscribe") {
|
|
746
960
|
const uri = jsonRpcMessage.params?.uri;
|
|
747
961
|
if (uri) {
|
|
748
962
|
session.subscriptions.delete(uri);
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
963
|
+
sessionLogger.info("subscription.removed", {
|
|
964
|
+
sessionId: session.sessionId,
|
|
965
|
+
uri,
|
|
966
|
+
totalSubscriptions: session.subscriptions.size
|
|
967
|
+
});
|
|
753
968
|
}
|
|
969
|
+
requestLogger.info("complete", {
|
|
970
|
+
method: "POST",
|
|
971
|
+
route: "/mcp",
|
|
972
|
+
sessionId: session.sessionId,
|
|
973
|
+
rpcMethod: jsonRpcMessage.method,
|
|
974
|
+
rpcId: jsonRpcMessage.id ?? null,
|
|
975
|
+
status: 200,
|
|
976
|
+
elapsedMs: Date.now() - requestStartedAt
|
|
977
|
+
});
|
|
754
978
|
return c.json({ jsonrpc: "2.0", result: {}, id: jsonRpcMessage.id });
|
|
755
979
|
}
|
|
756
|
-
|
|
980
|
+
transportLogger.debug("request.transport", {
|
|
981
|
+
method: "POST",
|
|
982
|
+
route: "/mcp",
|
|
983
|
+
sessionId: session.sessionId,
|
|
984
|
+
rpcMethod: jsonRpcMessage.method ?? null,
|
|
985
|
+
rpcId: jsonRpcMessage.id ?? null,
|
|
986
|
+
elapsedMs: Date.now() - requestStartedAt
|
|
987
|
+
});
|
|
988
|
+
const response = await session.transport.handleRequest(c);
|
|
989
|
+
requestLogger.info("complete", {
|
|
990
|
+
method: "POST",
|
|
991
|
+
route: "/mcp",
|
|
992
|
+
sessionId: session.sessionId,
|
|
993
|
+
rpcMethod: jsonRpcMessage.method ?? null,
|
|
994
|
+
rpcId: jsonRpcMessage.id ?? null,
|
|
995
|
+
status: response.status,
|
|
996
|
+
elapsedMs: Date.now() - requestStartedAt
|
|
997
|
+
});
|
|
998
|
+
return response;
|
|
757
999
|
});
|
|
758
1000
|
app.get("/mcp", async (c) => {
|
|
759
1001
|
const sessionId = c.req.header("mcp-session-id");
|
|
@@ -784,7 +1026,7 @@ app.get("/mcp", async (c) => {
|
|
|
784
1026
|
stream.onAbort(() => {
|
|
785
1027
|
clearInterval(pingInterval);
|
|
786
1028
|
session.sseWriters.delete(writer);
|
|
787
|
-
|
|
1029
|
+
sseLogger.info("client.disconnected", { sessionId });
|
|
788
1030
|
});
|
|
789
1031
|
let keepAlive = true;
|
|
790
1032
|
while (keepAlive) {
|
|
@@ -807,7 +1049,7 @@ app.delete("/mcp", async (c) => {
|
|
|
807
1049
|
return c.text("Session not found", 404);
|
|
808
1050
|
}
|
|
809
1051
|
sessions.delete(sessionId);
|
|
810
|
-
|
|
1052
|
+
sessionLogger.info("terminated", { sessionId });
|
|
811
1053
|
return c.body(null, 200);
|
|
812
1054
|
});
|
|
813
1055
|
let transcriptionWorker = null;
|
|
@@ -875,8 +1117,10 @@ async function main() {
|
|
|
875
1117
|
composeModel: args.composeModel ?? process.env.PROTOKOLL_COMPOSE_MODEL,
|
|
876
1118
|
transcriptionModel: args.transcriptionModel ?? process.env.PROTOKOLL_TRANSCRIPTION_MODEL,
|
|
877
1119
|
debug: args.debug ?? parseBooleanEnv(process.env.PROTOKOLL_DEBUG),
|
|
878
|
-
verbose: args.verbose ?? parseBooleanEnv(process.env.PROTOKOLL_VERBOSE)
|
|
1120
|
+
verbose: args.verbose ?? parseBooleanEnv(process.env.PROTOKOLL_VERBOSE),
|
|
1121
|
+
storage: buildEnvStorageConfig()
|
|
879
1122
|
});
|
|
1123
|
+
configureHttpLogLevel(cardigantimeConfig.debug === true);
|
|
880
1124
|
if (!args.config) {
|
|
881
1125
|
const resolvedConfigDirs = cardigantimeConfig.resolvedConfigDirs;
|
|
882
1126
|
process.env.WORKSPACE_ROOT = resolvedConfigDirs?.[0] ?? process.env.WORKSPACE_ROOT ?? process.cwd();
|
|
@@ -912,24 +1156,26 @@ async function main() {
|
|
|
912
1156
|
const outputDir = cardigantimeConfig.outputDirectory || "NOT SET";
|
|
913
1157
|
const contextDirs = cardigantimeConfig.contextDirectories;
|
|
914
1158
|
const contextDirsDisplay = Array.isArray(contextDirs) && contextDirs.length > 0 ? contextDirs.join(", ") : "NOT SET";
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1159
|
+
lifecycleLogger.info("startup", {
|
|
1160
|
+
serverName: "protokoll-mcp-http",
|
|
1161
|
+
transport: "hono",
|
|
1162
|
+
serverUrl: `http://${host}:${port}`,
|
|
1163
|
+
healthEndpoint: `http://${host}:${port}/health`,
|
|
1164
|
+
mcpEndpoint: `http://${host}:${port}/mcp`,
|
|
1165
|
+
portSource,
|
|
1166
|
+
workingDirectory: process.cwd(),
|
|
1167
|
+
configFile: configPathDisplay,
|
|
1168
|
+
inputDirectory: inputDir,
|
|
1169
|
+
outputDirectory: outputDir,
|
|
1170
|
+
contextDirectories: contextDirsDisplay,
|
|
1171
|
+
model: cardigantimeConfig.model || "default",
|
|
1172
|
+
transcriptionModel: cardigantimeConfig.transcriptionModel || "default",
|
|
1173
|
+
debugMode: cardigantimeConfig.debug ? "ON" : "OFF",
|
|
1174
|
+
verboseMode: cardigantimeConfig.verbose ? "ON" : "OFF"
|
|
1175
|
+
});
|
|
1176
|
+
lifecycleLogger.debug("startup.storage", {
|
|
1177
|
+
lines: describeRawStorageConfig(cardigantimeConfig)
|
|
1178
|
+
});
|
|
933
1179
|
if (outputDir !== "NOT SET") {
|
|
934
1180
|
const uploadDirectory = join(outputDir, "uploads");
|
|
935
1181
|
transcriptionWorker = new TranscriptionWorker({
|
|
@@ -939,6 +1185,7 @@ async function main() {
|
|
|
939
1185
|
// uses the same entity store as the rest of the server (not guessed from CWD).
|
|
940
1186
|
contextDirectories: Array.isArray(contextDirs) && contextDirs.length > 0 ? contextDirs : void 0,
|
|
941
1187
|
uploadDirectory,
|
|
1188
|
+
outputStorage: getOutputStorage(),
|
|
942
1189
|
scanInterval: 5e3,
|
|
943
1190
|
// 5 second scan interval
|
|
944
1191
|
model: cardigantimeConfig.model,
|
|
@@ -947,17 +1194,19 @@ async function main() {
|
|
|
947
1194
|
await transcriptionWorker.start();
|
|
948
1195
|
setWorkerInstance(transcriptionWorker);
|
|
949
1196
|
} else {
|
|
950
|
-
|
|
1197
|
+
lifecycleLogger.info("worker.disabled", {
|
|
1198
|
+
reason: "output_directory_not_configured"
|
|
1199
|
+
});
|
|
951
1200
|
}
|
|
952
1201
|
process.on("SIGTERM", async () => {
|
|
953
|
-
|
|
1202
|
+
lifecycleLogger.info("shutdown.signal_received", { signal: "SIGTERM" });
|
|
954
1203
|
if (transcriptionWorker) {
|
|
955
1204
|
await transcriptionWorker.stop();
|
|
956
1205
|
}
|
|
957
1206
|
process.exit(0);
|
|
958
1207
|
});
|
|
959
1208
|
process.on("SIGINT", async () => {
|
|
960
|
-
|
|
1209
|
+
lifecycleLogger.info("shutdown.signal_received", { signal: "SIGINT" });
|
|
961
1210
|
if (transcriptionWorker) {
|
|
962
1211
|
await transcriptionWorker.stop();
|
|
963
1212
|
}
|
|
@@ -970,7 +1219,9 @@ async function main() {
|
|
|
970
1219
|
});
|
|
971
1220
|
}
|
|
972
1221
|
main().catch((error) => {
|
|
973
|
-
|
|
1222
|
+
lifecycleLogger.error("startup.failed", {
|
|
1223
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1224
|
+
});
|
|
974
1225
|
process.exit(1);
|
|
975
1226
|
});
|
|
976
1227
|
|