@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.
@@ -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, j as getServerConfig, k as getContext, t as tools, h as handleToolCall, a as handleListResources, b as handleReadResource, g as getPrompts, c as getPrompt, l as createQuietLogger, e as getCachedRoots, m as setWorkerInstance } from '../configDiscovery.js';
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
- const byHash = await glob(`${item.metadata.audioHash}.*`, { cwd: this.config.uploadDirectory, absolute: true });
165
- if (byHash.length > 0) {
166
- audioFilePath = byHash[0];
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
- throw new Error(`Audio file not found at ${audioFilePath} and no file matching hash ${item.metadata.audioHash} in uploads`);
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
- console.log(`šŸ”” Entity change notification sent for ${entityUri} (${notified} writer(s) notified)`);
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
- console.warn(`sendEntityChangeNotifications: error for ${toolName}:`, err);
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
- console.log(`Cleaning up inactive session: ${sessionId}`);
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
- console.error("Failed to send entity change notifications:", err);
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 uploadDir = join(outputDir, "uploads");
519
- await fs.mkdir(uploadDir, { recursive: true });
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 uploadedPath = join(uploadDir, `${hash}.${ext}`);
523
- await fs.writeFile(uploadedPath, Buffer.from(buffer));
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(uploadedPath),
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
- console.log(`
538
- šŸ“¤ Audio uploaded: ${file.name} → ${uuid}`);
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
- console.error("Upload error:", error);
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 uploadDir = join(outputDir, "uploads");
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 audioFiles = await glob(`${metadata.audioHash}.*`, { cwd: uploadDir, absolute: true });
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 stat = await fs.stat(audioFile);
736
+ const audioBuffer = await outputStorage.readFile(audioFile);
579
737
  c.header("Content-Type", getAudioMimeType(ext));
580
- c.header("Content-Length", stat.size.toString());
738
+ c.header("Content-Length", audioBuffer.length.toString());
581
739
  c.header("Content-Disposition", `attachment; filename="${metadata.audioFile || `${uuid}${ext}`}"`);
582
- const stream = createReadStream(audioFile);
583
- return c.body(stream);
740
+ return c.body(audioBuffer);
584
741
  } catch (error) {
585
- console.error("Download error:", error);
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
- if (!sessionIdHeader && !isInitialize) {
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
- session = {
650
- sessionId: newSessionId,
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(newSessionId, session);
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 workspaceRoot = process.env.WORKSPACE_ROOT || process.cwd();
664
- const initialRoots = [{
665
- uri: `file://${workspaceRoot}`,
666
- name: "Workspace"
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
- console.log("\n=================================================================");
675
- console.log("SESSION CREATED");
676
- console.log("=================================================================");
677
- console.log(`Session ID: ${newSessionId}`);
678
- console.log(`Working Directory: ${workspaceRoot}`);
679
- console.log(`Config File: ${configPathDisplay}`);
680
- console.log("\nCONFIGURATION LOADED:");
681
- console.log("-----------------------------------------------------------------");
682
- console.log(`šŸ“„ Input (Audio): ${serverConfig.inputDirectory || "NOT SET"}`);
683
- console.log(`šŸ“¤ Output (Notes): ${serverConfig.outputDirectory || "NOT SET"}`);
684
- console.log(`āœ… Processed (Done): ${serverConfig.processedDirectory || "NOT SET"}`);
685
- console.log(`šŸ“š Context Dirs: ${contextDirsDisplay}`);
686
- console.log(`šŸ¤– AI Model: ${cardigantimeConfig.model || "default"}`);
687
- console.log(`šŸŽ¤ Transcribe Model: ${cardigantimeConfig.transcriptionModel || "default"}`);
688
- console.log(`šŸ› Debug Mode: ${cardigantimeConfig.debug ? "ON" : "OFF"}`);
689
- console.log(`šŸ“¢ Verbose Mode: ${cardigantimeConfig.verbose ? "ON" : "OFF"}`);
690
- console.log("=================================================================\n");
691
- c.header("Mcp-Session-Id", newSessionId);
692
- await session.server.connect(transport);
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
- session = sessions.get(sessionIdHeader);
877
+ const requestedSessionId = sessionIdHeader?.trim() || randomUUID();
878
+ session = sessions.get(requestedSessionId);
695
879
  if (!session) {
696
- return c.json({
697
- jsonrpc: "2.0",
698
- error: { code: -32e3, message: "Session not found" },
699
- id: jsonRpcMessage.id || null
700
- }, 404);
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
- console.log("\n─────────────────────────────────────────────────────────────────");
706
- console.log(`šŸ“Ø Incoming ${isNotification ? "NOTIFICATION" : "REQUEST"}`);
707
- console.log("─────────────────────────────────────────────────────────────────");
708
- console.log(`Method: ${jsonRpcMessage.method}`);
709
- console.log(`ID: ${jsonRpcMessage.id !== void 0 ? jsonRpcMessage.id : "(none - notification)"}`);
710
- console.log(`Session: ${sessionIdHeader || "(new session)"}`);
711
- if (jsonRpcMessage.params && Object.keys(jsonRpcMessage.params).length > 0) {
712
- console.log(`Parameters:`);
713
- console.log(JSON.stringify(jsonRpcMessage.params, null, 2));
714
- } else {
715
- console.log(`Parameters: (none)`);
716
- }
717
- console.log("─────────────────────────────────────────────────────────────────");
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
- console.log("āœ… Client initialized");
910
+ requestLogger.info("notification.initialized", {
911
+ sessionId: session.sessionId,
912
+ rpcMethod: jsonRpcMessage.method
913
+ });
723
914
  break;
724
915
  case "notifications/cancelled":
725
- console.log("āš ļø Client cancelled request:", jsonRpcMessage.params);
916
+ requestLogger.info("notification.cancelled", {
917
+ sessionId: session.sessionId,
918
+ params: jsonRpcMessage.params ?? null
919
+ });
726
920
  break;
727
921
  default:
728
- console.log("āš ļø Unknown notification:", jsonRpcMessage.method);
922
+ requestLogger.debug("notification.unknown", {
923
+ sessionId: session.sessionId,
924
+ rpcMethod: jsonRpcMessage.method
925
+ });
729
926
  }
730
- console.log("šŸ“¤ Response: 202 Accepted (notification)");
731
- console.log("─────────────────────────────────────────────────────────────────\n");
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
- console.log(`
739
- šŸ“ SUBSCRIPTION CREATED: ${uri} (session: ${session.sessionId})`);
740
- console.log(` Total subscriptions for session: ${session.subscriptions.size}
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
- console.log(`
750
- šŸ“ SUBSCRIPTION REMOVED: ${uri} (session: ${session.sessionId})`);
751
- console.log(` Total subscriptions for session: ${session.subscriptions.size}
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
- return session.transport.handleRequest(c);
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
- console.log(`SSE client disconnected from session ${sessionId}`);
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
- console.log(`Session terminated: ${sessionId}`);
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
- console.log("\n=================================================================");
916
- console.log("PROTOKOLL MCP HTTP SERVER (Hono)");
917
- console.log("=================================================================");
918
- console.log(`🌐 Server URL: http://${host}:${port} ${portSource}`);
919
- console.log(`šŸ’š Health Check: http://${host}:${port}/health`);
920
- console.log(`šŸ”Œ MCP Endpoint: http://${host}:${port}/mcp`);
921
- console.log(`šŸ“ Working Directory: ${process.cwd()}`);
922
- console.log(`āš™ļø Config File: ${configPathDisplay}`);
923
- console.log("\nCONFIGURATION:");
924
- console.log("-----------------------------------------------------------------");
925
- console.log(`šŸ“„ Input (Audio): ${inputDir}`);
926
- console.log(`šŸ“¤ Output (Notes): ${outputDir}`);
927
- console.log(`šŸ“š Context Dirs: ${contextDirsDisplay}`);
928
- console.log(`šŸ¤– AI Model: ${cardigantimeConfig.model || "default"}`);
929
- console.log(`šŸŽ¤ Transcribe Model: ${cardigantimeConfig.transcriptionModel || "default"}`);
930
- console.log(`šŸ› Debug Mode: ${cardigantimeConfig.debug ? "ON" : "OFF"}`);
931
- console.log(`šŸ“¢ Verbose Mode: ${cardigantimeConfig.verbose ? "ON" : "OFF"}`);
932
- console.log("=================================================================\n");
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
- console.log("āš ļø Output directory not configured - transcription worker disabled");
1197
+ lifecycleLogger.info("worker.disabled", {
1198
+ reason: "output_directory_not_configured"
1199
+ });
951
1200
  }
952
1201
  process.on("SIGTERM", async () => {
953
- console.log("\nšŸ›‘ Received SIGTERM, shutting down gracefully...");
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
- console.log("\nšŸ›‘ Received SIGINT, shutting down gracefully...");
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
- console.error("Failed to start server:", error);
1222
+ lifecycleLogger.error("startup.failed", {
1223
+ error: error instanceof Error ? error.message : String(error)
1224
+ });
974
1225
  process.exit(1);
975
1226
  });
976
1227