@json-to-office/mcp-server 1.5.0 → 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/index.d.ts CHANGED
@@ -338,8 +338,18 @@ declare function createOutputRoot(options?: OutputRootOptions): OutputRoot;
338
338
  * failures (see `errors.ts`).
339
339
  */
340
340
 
341
+ /**
342
+ * A success may still have something to say.
343
+ *
344
+ * `warnings` is how a store reports what did not go wrong enough to fail the
345
+ * call — a revision that did not reach disk (#290) being the case that
346
+ * matters: the edit landed, the handle works, and the agent still needs to
347
+ * know it has stopped being recoverable. Tools fold these into the
348
+ * `diagnostics` of the result they were already returning.
349
+ */
341
350
  type WorkspaceResult<T> = ({
342
351
  ok: true;
352
+ warnings?: Diagnostic[];
343
353
  } & T) | Failure;
344
354
  /** RFC 6902 operation. Paths are RFC 6901 pointers — no private dialect (#271). */
345
355
  interface JsonPatchOperation {
@@ -363,6 +373,11 @@ interface WorkspaceRecord {
363
373
  title?: string;
364
374
  /** Revisions pinned by `snapshot`, still retrievable through `get`. */
365
375
  pinnedRevisions: number[];
376
+ /**
377
+ * Whether this revision is on disk and so survives the connection (#290).
378
+ * Absent when the store has no durable backing at all, which is the default.
379
+ */
380
+ persisted?: boolean;
366
381
  }
367
382
  interface WorkspaceStore {
368
383
  /**
@@ -436,7 +451,9 @@ interface WorkspaceStore {
436
451
  *
437
452
  * A connection's store is reachable only through its `ToolDeps`, so the
438
453
  * documents go when the connection's deps do; this is for a host that wants
439
- * the memory back at a moment of its own choosing.
454
+ * the memory back at a moment of its own choosing. It releases memory only —
455
+ * a durable store keeps its copies, because an event that ends a connection
456
+ * is exactly what #290 stops being fatal.
440
457
  */
441
458
  closeAll(): Promise<void>;
442
459
  }
@@ -452,6 +469,140 @@ declare const unavailableWorkspaceStore: WorkspaceStore;
452
469
  declare function setWorkspaceStore(store: WorkspaceStore | undefined): void;
453
470
  declare function getWorkspaceStore(): WorkspaceStore;
454
471
 
472
+ /**
473
+ * Disk backing for workspaces (#290).
474
+ *
475
+ * #271 made the document JSON authoritative and then kept it in exactly one
476
+ * place: server memory. Anything that ends a connection — a host session
477
+ * reset, a client restart, a crash — took every revision with it, and
478
+ * `jto_workspace_snapshot` did not help because the pins lived in the same
479
+ * `Map`. This module is the durable half: memory stays the fast path and the
480
+ * only thing tools read from, and every committed revision is mirrored here so
481
+ * a reconnecting client can resume instead of re-authoring.
482
+ *
483
+ * Off unless a root is configured (`--workspace-dir`, `JTO_MCP_WORKSPACE_DIR`),
484
+ * because turning handles durable turns them cross-connection, and a server
485
+ * that started leaving documents on a user's disk without being asked would be
486
+ * a surprise rather than a feature.
487
+ *
488
+ * Three properties the layout is chosen for:
489
+ *
490
+ * - Crash safety. A revision file is written and renamed into place BEFORE the
491
+ * metadata that names it, so `meta.json` never points at bytes that are not
492
+ * there. Interrupt this at any point and what is on disk is either the old
493
+ * revision or the new one, never half of either.
494
+ * - Cheap listing. Recovering handles after a reset reads one small
495
+ * `meta.json` per workspace; the documents are only paid for when a handle
496
+ * is actually used.
497
+ * - No caller-named paths. A handle is the only thing that reaches the
498
+ * filesystem, and it is matched against a whitelist first, so nothing a
499
+ * client sends can address a directory of its choosing.
500
+ *
501
+ * Nothing here is a tool result, so these functions throw rather than return
502
+ * diagnostics; the store catches and downgrades a write failure to a warning
503
+ * (`W_WORKSPACE_NOT_PERSISTED`), because losing durability is worth saying out
504
+ * loud and is never worth failing an edit over.
505
+ *
506
+ * Not a concurrency layer. Individual writes are atomic, so a root two
507
+ * connections share never tears — but each holds its own memory copy of an
508
+ * entry, and the second to commit a revision is the one on disk. The intended
509
+ * arrangement is one root per client, which is what a reconnect is.
510
+ */
511
+
512
+ /** Env var that names the workspace root, below the `--workspace-dir` flag. */
513
+ declare const WORKSPACE_DIR_ENV = "JTO_MCP_WORKSPACE_DIR";
514
+ interface PersistenceLimits {
515
+ /** Workspaces kept on disk; the least recently updated go first. */
516
+ maxWorkspaces: number;
517
+ /** Revision files per workspace — the head plus its pinned revisions. */
518
+ maxRevisionsPerWorkspace: number;
519
+ /** Ceiling for a single revision file. */
520
+ maxEntryBytes: number;
521
+ }
522
+ /**
523
+ * Roomier than the memory limits on purpose.
524
+ *
525
+ * Memory bounds protect a process; these bound a directory that outlives it,
526
+ * where the cost of keeping a document is a few hundred kilobytes of disk and
527
+ * the cost of dropping one is authoring an agent has to redo. `maxWorkspaces`
528
+ * is twice the memory ceiling so a reconnecting client still finds what the
529
+ * previous connection had open, and the revision cap is the head plus the
530
+ * default eight pins, so a default setup never has to discard a pin it took.
531
+ */
532
+ declare const DEFAULT_PERSISTENCE_LIMITS: PersistenceLimits;
533
+ /** One retained revision: the serialized document and its size. */
534
+ interface PersistedDocument {
535
+ revision: number;
536
+ text: string;
537
+ bytes: number;
538
+ }
539
+ /** What the store hands over to be made durable. */
540
+ interface PersistedSnapshot {
541
+ handle: string;
542
+ format: FormatName;
543
+ createdAt: number;
544
+ updatedAt: number;
545
+ title?: string;
546
+ /** The current revision. */
547
+ head: PersistedDocument;
548
+ /** Revisions pinned by `snapshot`, oldest first. */
549
+ pins: readonly PersistedDocument[];
550
+ }
551
+ /** A workspace as `list` reports it, without paying for the documents. */
552
+ interface PersistedMeta {
553
+ handle: string;
554
+ format: FormatName;
555
+ revision: number;
556
+ bytes: number;
557
+ createdAt: number;
558
+ updatedAt: number;
559
+ title?: string;
560
+ pinnedRevisions: number[];
561
+ }
562
+ /** A workspace read back whole, ready to become a live entry again. */
563
+ interface RestoredWorkspace extends PersistedMeta {
564
+ head: PersistedDocument;
565
+ pins: PersistedDocument[];
566
+ }
567
+ interface WorkspacePersistence {
568
+ /** Absolute path of the root. May not exist on disk until first write. */
569
+ readonly root: string;
570
+ readonly limits: PersistenceLimits;
571
+ /** Mirror a workspace's current state. Overwrites whatever was there. */
572
+ save(snapshot: PersistedSnapshot): Promise<void>;
573
+ /** Everything on disk, newest update first. Corrupt entries are skipped. */
574
+ list(): Promise<PersistedMeta[]>;
575
+ /** One workspace, documents included, or undefined when it is not there. */
576
+ load(handle: string): Promise<RestoredWorkspace | undefined>;
577
+ /**
578
+ * Forget a workspace durably. Idempotent; answers whether anything was
579
+ * there, which is how `close` reports a handle it only ever saw on disk.
580
+ *
581
+ * Throws when the directory is there and could not be deleted. That has to
582
+ * reach the caller: `jto_workspace_close` promises the document is gone, and
583
+ * a swallowed failure would report it closed while the JSON stayed readable
584
+ * by the next connection to use the handle.
585
+ */
586
+ remove(handle: string): Promise<boolean>;
587
+ }
588
+ interface WorkspacePersistenceOptions extends Partial<PersistenceLimits> {
589
+ /** `--workspace-dir` value, highest precedence. */
590
+ flagDir?: string;
591
+ /** Defaults to `process.env`; injectable so the precedence is testable. */
592
+ env?: NodeJS.ProcessEnv;
593
+ }
594
+ /**
595
+ * Build the persistence layer, or answer `undefined` when none was configured.
596
+ *
597
+ * `undefined` rather than a no-op implementation: the store branches on
598
+ * whether it has durability at all — to decide what `jto_info` reports and
599
+ * whether an evicted handle is recoverable — and a stand-in that quietly
600
+ * discarded writes would make that question unanswerable.
601
+ */
602
+ declare function createWorkspacePersistence(options?: WorkspacePersistenceOptions): WorkspacePersistence | undefined;
603
+ /** The same layer on an explicit directory, for hosts and tests. */
604
+ declare function createWorkspacePersistenceAt(root: string, limits?: Partial<PersistenceLimits>): WorkspacePersistence;
605
+
455
606
  /**
456
607
  * What every tool module is handed at registration.
457
608
  *
@@ -474,6 +625,15 @@ interface ToolDeps {
474
625
  * registered, and a snapshot taken at registration would pin the stand-in.
475
626
  */
476
627
  workspaces(): WorkspaceStore;
628
+ /**
629
+ * Disk backing for that store (#290), when a root was configured.
630
+ *
631
+ * Undefined is the default and means memory-only handles. It lives here
632
+ * rather than inside the store because `tools/workspace.ts` is what builds
633
+ * the connection's store, and `jto_info` reports the root without touching
634
+ * one.
635
+ */
636
+ workspacePersistence?: WorkspacePersistence;
477
637
  /** Ceiling for `outputMode: 'base64'`, in bytes. */
478
638
  maxInlineArtifactBytes: number;
479
639
  }
@@ -481,6 +641,10 @@ interface CreateToolDepsOptions {
481
641
  /** `--output-dir`, or an already-built root (tests hand one in). */
482
642
  outputDir?: string;
483
643
  outputRoot?: OutputRoot;
644
+ /** `--workspace-dir`. Absent, `JTO_MCP_WORKSPACE_DIR` decides. */
645
+ workspaceDir?: string;
646
+ /** An already-built persistence layer, for hosts and tests. */
647
+ workspacePersistence?: WorkspacePersistence;
484
648
  env?: NodeJS.ProcessEnv;
485
649
  serverVersion?: string;
486
650
  workspaces?: () => WorkspaceStore;
@@ -724,10 +888,112 @@ declare function parseDocumentJson(text: string, path?: string): {
724
888
  document: unknown;
725
889
  } | Failure;
726
890
 
891
+ /**
892
+ * The in-memory workspace store (#271).
893
+ *
894
+ * Documents are held as **serialized JSON text**, not as live trees. That one
895
+ * decision buys three of the issue's requirements outright:
896
+ *
897
+ * - Isolation. A read is a fresh `JSON.parse`, so a caller can mutate what it
898
+ * got and two workspaces can never share a subtree — the structural-sharing
899
+ * bug this feature invites cannot be written.
900
+ * - Atomicity. A commit is a single string assignment after a successful
901
+ * apply; a patch that fails leaves the stored bytes untouched, literally.
902
+ * - Accounting. `bytes` is the real size, already computed, so the count and
903
+ * byte budgets are exact rather than estimated.
904
+ *
905
+ * The cost is a parse and a stringify per operation, which for documents in
906
+ * the tens to hundreds of kilobytes this format produces is far below the
907
+ * round trip that made the agent call us.
908
+ *
909
+ * Nothing here throws: an unknown handle, a stale revision and a full store
910
+ * are answers an agent repairs, so they come back as structured failures
911
+ * (`lib/errors.ts`) exactly like a bad document does.
912
+ */
913
+
914
+ /**
915
+ * Workspace-lifecycle codes.
916
+ *
917
+ * Kept here rather than in `lib/errors.ts` (owned by #203, edited
918
+ * concurrently); they read as ordinary diagnostic codes to a client. TTL
919
+ * eviction gets its own code rather than reusing `E_UNKNOWN_HANDLE` because
920
+ * an agent can reopen and carry on after an idle handle expires.
921
+ */
922
+ declare const WORKSPACE_ERROR_CODES: {
923
+ readonly EVICTED: "E_WORKSPACE_EVICTED";
924
+ readonly LIMIT: "E_WORKSPACE_LIMIT";
925
+ readonly DOCUMENT_TOO_LARGE: "E_DOCUMENT_TOO_LARGE";
926
+ readonly INVALID_ROOT: "E_INVALID_DOCUMENT_ROOT";
927
+ /**
928
+ * A revision could not be mirrored to disk (#290). A warning, never a
929
+ * failure: the edit landed and the workspace is usable, but this connection
930
+ * has stopped being survivable and saying so is the whole point.
931
+ */
932
+ readonly NOT_PERSISTED: "W_WORKSPACE_NOT_PERSISTED";
933
+ /**
934
+ * A close could not delete the durable copy, so nothing was released (#290).
935
+ * An error rather than a warning: the agent asked for the document to be
936
+ * destroyed, and it is still there to be read by the next connection.
937
+ */
938
+ readonly NOT_CLOSED: "E_WORKSPACE_NOT_CLOSED";
939
+ };
940
+ interface WorkspaceLimits {
941
+ /** Open documents at once. */
942
+ maxWorkspaces: number;
943
+ /** Ceiling for a single serialized document. */
944
+ maxDocumentBytes: number;
945
+ /** Ceiling for every document and pinned snapshot together. */
946
+ maxTotalBytes: number;
947
+ /** Idle time after which a handle is dropped. Any read or write resets it. */
948
+ idleTtlMs: number;
949
+ /** Snapshots kept retrievable per workspace; new pins are refused at the cap. */
950
+ maxPinnedRevisions: number;
951
+ }
952
+ /**
953
+ * Deliberately modest.
954
+ *
955
+ * A workspace is a live authoring buffer for one agent on one stdio
956
+ * connection, not a document store: sixteen open documents is already more
957
+ * than an agent can hold in context, and the byte ceilings exist to stop a
958
+ * runaway loop from turning a helper process into the machine's memory
959
+ * problem. The idle TTL is half an hour because an agent's turn can stall on a
960
+ * human for a long while, and losing a document mid-conversation is worse than
961
+ * holding a few megabytes.
962
+ */
963
+ declare const DEFAULT_WORKSPACE_LIMITS: WorkspaceLimits;
964
+ interface MemoryWorkspaceStoreOptions extends Partial<WorkspaceLimits> {
965
+ /** Injectable clock; the TTL suite drives it instead of waiting. */
966
+ now?: () => number;
967
+ /** Injectable handle source, for tests that need predictable handles. */
968
+ newHandle?: () => string;
969
+ /**
970
+ * Disk backing (#290). Absent is the historical behaviour: memory only, and
971
+ * everything goes when the connection does.
972
+ */
973
+ persistence?: WorkspacePersistence;
974
+ }
975
+ interface MemoryWorkspaceStore extends WorkspaceStore {
976
+ readonly limits: WorkspaceLimits;
977
+ /** Live totals, for `jto_workspace_list` to show the agent its budget. */
978
+ usage(): {
979
+ workspaces: number;
980
+ bytes: number;
981
+ };
982
+ /**
983
+ * Where revisions survive the connection, when a root was configured.
984
+ * Absent means handles are memory-only and end with this process (#290).
985
+ */
986
+ readonly persistence?: {
987
+ root: string;
988
+ limits: PersistenceLimits;
989
+ };
990
+ }
991
+ declare function createMemoryWorkspaceStore(options?: MemoryWorkspaceStoreOptions): MemoryWorkspaceStore;
992
+
727
993
  declare const SERVER_VERSION: string;
728
994
  /** MCP `serverInfo.name`. Stable: clients key configuration off it. */
729
995
  declare const SERVER_NAME = "json-to-office";
730
996
  /** npm identity, reported by `jto_info` next to the workspace packages. */
731
997
  declare const PACKAGE_NAME = "@json-to-office/mcp-server";
732
998
 
733
- export { type Artifact, type ArtifactMode, type ArtifactOutputInput, type CreateToolDepsOptions, DOCUMENT_SOURCE_RULE, type DeliverArtifactOptions, type DeliverArtifactResult, type Diagnostic, type DiagnosticCounts, type DiagnosticSeverity, type DocumentSourceInput, ERROR_CODES, type ErrorCode, FORMAT_NAMES, type Failure, type JsonPatchOperation, MAX_INLINE_ARTIFACT_BYTES, MIME_TYPES, OPTION_ERROR_CODES, OUTPUT_DIR_ENV, type OutputRoot, type OutputRootOptions, PACKAGE_NAME, type RenderOptionsInput, type ResolvedDocument, type ResolvedOutputPath, S, SERVER_INSTRUCTIONS, SERVER_NAME, SERVER_VERSION, type SourceSummary, type ToolDeps, type ToolEnvelope, type WorkspaceRecord, type WorkspaceResult, type WorkspaceStore, artifactOutputProperties, artifactSchema, checkOutputName, checkRenderer, countDiagnostics, createOutputRoot, createServer, createServerFactory, createToolDeps, deliverArtifact, diagnostic, diagnosticSchema, diagnosticsFromThrown, diagnosticsSchema, documentSourceProperties, documentSourceSchema, envelopeProperties, failure, failureFrom, formatSchema, fromValidationError, fromValidationErrors, getAdapter, getWorkspaceStore, guarded, outputSchema, parseDocumentJson, renderOptionProperties, resetAdapters, resolveDocumentSource, setWorkspaceStore, sourceSummary, sourceSummarySchema, success, toJsonPointer, toolResult, unavailableWorkspaceStore, validationDiagnostics, withRenderer };
999
+ export { type Artifact, type ArtifactMode, type ArtifactOutputInput, type CreateToolDepsOptions, DEFAULT_PERSISTENCE_LIMITS, DEFAULT_WORKSPACE_LIMITS, DOCUMENT_SOURCE_RULE, type DeliverArtifactOptions, type DeliverArtifactResult, type Diagnostic, type DiagnosticCounts, type DiagnosticSeverity, type DocumentSourceInput, ERROR_CODES, type ErrorCode, FORMAT_NAMES, type Failure, type JsonPatchOperation, MAX_INLINE_ARTIFACT_BYTES, MIME_TYPES, type MemoryWorkspaceStore, type MemoryWorkspaceStoreOptions, OPTION_ERROR_CODES, OUTPUT_DIR_ENV, type OutputRoot, type OutputRootOptions, PACKAGE_NAME, type PersistenceLimits, type RenderOptionsInput, type ResolvedDocument, type ResolvedOutputPath, S, SERVER_INSTRUCTIONS, SERVER_NAME, SERVER_VERSION, type SourceSummary, type ToolDeps, type ToolEnvelope, WORKSPACE_DIR_ENV, WORKSPACE_ERROR_CODES, type WorkspaceLimits, type WorkspacePersistence, type WorkspacePersistenceOptions, type WorkspaceRecord, type WorkspaceResult, type WorkspaceStore, artifactOutputProperties, artifactSchema, checkOutputName, checkRenderer, countDiagnostics, createMemoryWorkspaceStore, createOutputRoot, createServer, createServerFactory, createToolDeps, createWorkspacePersistence, createWorkspacePersistenceAt, deliverArtifact, diagnostic, diagnosticSchema, diagnosticsFromThrown, diagnosticsSchema, documentSourceProperties, documentSourceSchema, envelopeProperties, failure, failureFrom, formatSchema, fromValidationError, fromValidationErrors, getAdapter, getWorkspaceStore, guarded, outputSchema, parseDocumentJson, renderOptionProperties, resetAdapters, resolveDocumentSource, setWorkspaceStore, sourceSummary, sourceSummarySchema, success, toJsonPointer, toolResult, unavailableWorkspaceStore, validationDiagnostics, withRenderer };