open-mcp-app 0.0.14 → 0.0.15

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.
@@ -321,6 +321,35 @@ interface TransportSessionInfo {
321
321
  /** The transport type for this session */
322
322
  transport: TransportType;
323
323
  }
324
+ type ServerlessTransportMode = "stateful" | "stateless";
325
+ interface StateAdapter {
326
+ get: (instanceId: string) => Promise<unknown | undefined>;
327
+ set: (instanceId: string, state: unknown) => Promise<void>;
328
+ delete: (instanceId: string) => Promise<void>;
329
+ }
330
+ interface ServerlessAdapterOptions {
331
+ /**
332
+ * Controls whether MCP transport sessions are persisted between requests.
333
+ *
334
+ * - `"stateful"` keeps normal MCP session semantics and expects sticky routing.
335
+ * - `"stateless"` disables MCP transport session IDs so each request stands alone.
336
+ *
337
+ * For AWS Lambda and other serverless deployments, `"stateless"` is usually the
338
+ * safest default because requests may land on different warm instances.
339
+ */
340
+ transportMode?: ServerlessTransportMode;
341
+ /**
342
+ * Enables JSON POST responses for stateless streamable-http mode.
343
+ * This avoids relying on SSE session continuity in serverless environments.
344
+ */
345
+ enableJsonResponse?: boolean;
346
+ /**
347
+ * Optional backing store for `context.getState()` / `context.setState()`.
348
+ * This is only needed for tools that rely on server-side state across requests.
349
+ */
350
+ stateAdapter?: StateAdapter;
351
+ }
352
+ type AwsLambdaHandler = (...args: unknown[]) => Promise<unknown>;
324
353
  /**
325
354
  * App configuration.
326
355
  */
@@ -619,13 +648,22 @@ declare class App {
619
648
  * Get the Express application for serverless wrapping (e.g. Lambda).
620
649
  * Does NOT start listening on a port.
621
650
  */
622
- toExpressApp(): express__default.Express;
651
+ toExpressApp(options?: ServerlessAdapterOptions): express__default.Express;
652
+ toAwsLambda(options?: ServerlessAdapterOptions): AwsLambdaHandler;
623
653
  /**
624
654
  * Close a specific transport session.
625
655
  */
626
656
  closeTransportSession(sessionId: string): boolean;
627
657
  private getPort;
628
658
  private getCallerDir;
659
+ private lambdaEventToRequest;
660
+ private handleLambdaHttpRequest;
661
+ private writeLambdaResponse;
662
+ private isStatelessTransport;
663
+ private getStateAdapter;
664
+ private loadInstanceState;
665
+ private persistInstanceState;
666
+ private deleteInstanceState;
629
667
  /**
630
668
  * Handle an MCP JSON-RPC request directly.
631
669
  * Used for testing without starting a server.
@@ -1478,4 +1516,4 @@ declare const exp: {
1478
1516
  sampleMessage: typeof experimental_sampleMessage;
1479
1517
  };
1480
1518
 
1481
- export { App, type AppConfig, type DisplayMode, type IconConfig, type InstanceDestroyContext, type KvSearchResult, MIME_TYPES, type ResourceConfig, type ServerLogLevel, type ServerLogger, type ToolAnnotations, type ToolCallInfo, type ToolCallResultInfo, type ToolConfig, type ToolContext, type ToolHandler, type ToolResult, type ToolVisibility, type TransportSessionInfo, type TransportType, type VectorSearchResult, type WebSocketConnection, createApp, exp, experimental_blobDelete, experimental_blobDeleteSync, experimental_blobGet, experimental_blobGetSync, experimental_blobIsAvailable, experimental_blobList, experimental_blobListSync, experimental_blobPut, experimental_blobPutSync, experimental_deleteFile, experimental_deleteFileSync, experimental_exists, experimental_existsSync, experimental_getProjectId, experimental_getServerName, experimental_getWritableDirectory, experimental_kvDelete, experimental_kvDeleteSync, experimental_kvGet, experimental_kvGetSync, experimental_kvIsAvailable, experimental_kvList, experimental_kvListSync, experimental_kvSearch, experimental_kvSet, experimental_kvSetSync, experimental_mkdir, experimental_mkdirSync, experimental_readFile, experimental_readFileSync, experimental_readdir, experimental_readdirSync, experimental_rmdir, experimental_rmdirSync, experimental_sampleMessage, experimental_vectorDelete, experimental_vectorIsAvailable, experimental_vectorSearch, experimental_vectorUpsert, experimental_writeFile, experimental_writeFileSync, htmlLoader, isHtmlContent, loadHtml, svgToDataUri, wrapServer };
1519
+ export { App, type AppConfig, type AwsLambdaHandler, type DisplayMode, type IconConfig, type InstanceDestroyContext, type KvSearchResult, MIME_TYPES, type ResourceConfig, type ServerLogLevel, type ServerLogger, type ServerlessAdapterOptions, type ServerlessTransportMode, type StateAdapter, type ToolAnnotations, type ToolCallInfo, type ToolCallResultInfo, type ToolConfig, type ToolContext, type ToolHandler, type ToolResult, type ToolVisibility, type TransportSessionInfo, type TransportType, type VectorSearchResult, type WebSocketConnection, createApp, exp, experimental_blobDelete, experimental_blobDeleteSync, experimental_blobGet, experimental_blobGetSync, experimental_blobIsAvailable, experimental_blobList, experimental_blobListSync, experimental_blobPut, experimental_blobPutSync, experimental_deleteFile, experimental_deleteFileSync, experimental_exists, experimental_existsSync, experimental_getProjectId, experimental_getServerName, experimental_getWritableDirectory, experimental_kvDelete, experimental_kvDeleteSync, experimental_kvGet, experimental_kvGetSync, experimental_kvIsAvailable, experimental_kvList, experimental_kvListSync, experimental_kvSearch, experimental_kvSet, experimental_kvSetSync, experimental_mkdir, experimental_mkdirSync, experimental_readFile, experimental_readFileSync, experimental_readdir, experimental_readdirSync, experimental_rmdir, experimental_rmdirSync, experimental_sampleMessage, experimental_vectorDelete, experimental_vectorIsAvailable, experimental_vectorSearch, experimental_vectorUpsert, experimental_writeFile, experimental_writeFileSync, htmlLoader, isHtmlContent, loadHtml, svgToDataUri, wrapServer };
@@ -6796,6 +6796,8 @@ var require_dist = __commonJS({
6796
6796
  // src/server/app.ts
6797
6797
  import { randomUUID } from "crypto";
6798
6798
  import path2 from "path";
6799
+ import { Readable as Readable2 } from "stream";
6800
+ import { pipeline } from "stream/promises";
6799
6801
  import { fileURLToPath } from "url";
6800
6802
 
6801
6803
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
@@ -12342,7 +12344,7 @@ var toRequestError = (e) => {
12342
12344
  return new RequestError(e.message, { cause: e });
12343
12345
  };
12344
12346
  var GlobalRequest = global.Request;
12345
- var Request = class extends GlobalRequest {
12347
+ var Request2 = class extends GlobalRequest {
12346
12348
  constructor(input, options) {
12347
12349
  if (typeof input === "object" && getRequestCache in input) {
12348
12350
  input = input[getRequestCache]();
@@ -12375,7 +12377,7 @@ var newRequestFromIncoming = (method, url, headers, incoming, abortController) =
12375
12377
  };
12376
12378
  if (method === "TRACE") {
12377
12379
  init.method = "GET";
12378
- const req = new Request(url, init);
12380
+ const req = new Request2(url, init);
12379
12381
  Object.defineProperty(req, "method", {
12380
12382
  get() {
12381
12383
  return "TRACE";
@@ -12412,7 +12414,7 @@ var newRequestFromIncoming = (method, url, headers, incoming, abortController) =
12412
12414
  init.body = Readable.toWeb(incoming);
12413
12415
  }
12414
12416
  }
12415
- return new Request(url, init);
12417
+ return new Request2(url, init);
12416
12418
  };
12417
12419
  var getRequestCache = /* @__PURE__ */ Symbol("getRequestCache");
12418
12420
  var requestCache = /* @__PURE__ */ Symbol("requestCache");
@@ -12473,7 +12475,7 @@ var requestPrototype = {
12473
12475
  }
12474
12476
  });
12475
12477
  });
12476
- Object.setPrototypeOf(requestPrototype, Request.prototype);
12478
+ Object.setPrototypeOf(requestPrototype, Request2.prototype);
12477
12479
  var newRequest = (incoming, defaultHostname) => {
12478
12480
  const req = Object.create(requestPrototype);
12479
12481
  req[incomingKey] = incoming;
@@ -12539,15 +12541,17 @@ var Response2 = class _Response {
12539
12541
  this.#init = init;
12540
12542
  }
12541
12543
  if (typeof body === "string" || typeof body?.getReader !== "undefined" || body instanceof Blob || body instanceof Uint8Array) {
12542
- headers ||= init?.headers || { "content-type": "text/plain; charset=UTF-8" };
12543
- this[cacheKey] = [init?.status || 200, body, headers];
12544
+ ;
12545
+ this[cacheKey] = [init?.status || 200, body, headers || init?.headers];
12544
12546
  }
12545
12547
  }
12546
12548
  get headers() {
12547
12549
  const cache = this[cacheKey];
12548
12550
  if (cache) {
12549
12551
  if (!(cache[2] instanceof Headers)) {
12550
- cache[2] = new Headers(cache[2]);
12552
+ cache[2] = new Headers(
12553
+ cache[2] || { "content-type": "text/plain; charset=UTF-8" }
12554
+ );
12551
12555
  }
12552
12556
  return cache[2];
12553
12557
  }
@@ -12672,15 +12676,32 @@ var flushHeaders = (outgoing) => {
12672
12676
  };
12673
12677
  var responseViaCache = async (res, outgoing) => {
12674
12678
  let [status, body, header] = res[cacheKey];
12675
- if (header instanceof Headers) {
12679
+ let hasContentLength = false;
12680
+ if (!header) {
12681
+ header = { "content-type": "text/plain; charset=UTF-8" };
12682
+ } else if (header instanceof Headers) {
12683
+ hasContentLength = header.has("content-length");
12676
12684
  header = buildOutgoingHttpHeaders(header);
12685
+ } else if (Array.isArray(header)) {
12686
+ const headerObj = new Headers(header);
12687
+ hasContentLength = headerObj.has("content-length");
12688
+ header = buildOutgoingHttpHeaders(headerObj);
12689
+ } else {
12690
+ for (const key in header) {
12691
+ if (key.length === 14 && key.toLowerCase() === "content-length") {
12692
+ hasContentLength = true;
12693
+ break;
12694
+ }
12695
+ }
12677
12696
  }
12678
- if (typeof body === "string") {
12679
- header["Content-Length"] = Buffer.byteLength(body);
12680
- } else if (body instanceof Uint8Array) {
12681
- header["Content-Length"] = body.byteLength;
12682
- } else if (body instanceof Blob) {
12683
- header["Content-Length"] = body.size;
12697
+ if (!hasContentLength) {
12698
+ if (typeof body === "string") {
12699
+ header["Content-Length"] = Buffer.byteLength(body);
12700
+ } else if (body instanceof Uint8Array) {
12701
+ header["Content-Length"] = body.byteLength;
12702
+ } else if (body instanceof Blob) {
12703
+ header["Content-Length"] = body.size;
12704
+ }
12684
12705
  }
12685
12706
  outgoing.writeHead(status, header);
12686
12707
  if (typeof body === "string" || body instanceof Uint8Array) {
@@ -12774,9 +12795,9 @@ var responseViaResponseObject = async (res, outgoing, options = {}) => {
12774
12795
  };
12775
12796
  var getRequestListener = (fetchCallback, options = {}) => {
12776
12797
  const autoCleanupIncoming = options.autoCleanupIncoming ?? true;
12777
- if (options.overrideGlobalObjects !== false && global.Request !== Request) {
12798
+ if (options.overrideGlobalObjects !== false && global.Request !== Request2) {
12778
12799
  Object.defineProperty(global, "Request", {
12779
- value: Request
12800
+ value: Request2
12780
12801
  });
12781
12802
  Object.defineProperty(global, "Response", {
12782
12803
  value: Response2
@@ -14173,7 +14194,7 @@ var App = class {
14173
14194
  ws.close();
14174
14195
  this.instanceWebSockets.delete(instanceId);
14175
14196
  }
14176
- this.instanceState.delete(instanceId);
14197
+ this.deleteInstanceState(instanceId);
14177
14198
  console.log(`[MCP] Instance destroyed: ${instanceId}`);
14178
14199
  return true;
14179
14200
  }
@@ -14258,8 +14279,30 @@ var App = class {
14258
14279
  * Get the Express application for serverless wrapping (e.g. Lambda).
14259
14280
  * Does NOT start listening on a port.
14260
14281
  */
14261
- toExpressApp() {
14262
- return this.createExpressApp();
14282
+ toExpressApp(options) {
14283
+ return this.createExpressApp({ serverless: options });
14284
+ }
14285
+ toAwsLambda(options) {
14286
+ const serverlessOptions = {
14287
+ transportMode: "stateless",
14288
+ ...options
14289
+ };
14290
+ if (serverlessOptions.transportMode !== "stateless") {
14291
+ throw new Error("app.toAwsLambda() currently requires transportMode: 'stateless'");
14292
+ }
14293
+ const lambdaRuntime = globalThis.awslambda;
14294
+ if (!lambdaRuntime) {
14295
+ throw new Error("app.toAwsLambda() is only available inside the AWS Lambda runtime");
14296
+ }
14297
+ return lambdaRuntime.streamifyResponse(async (event, responseStream) => {
14298
+ const request = this.lambdaEventToRequest(event);
14299
+ const { response, cleanup } = await this.handleLambdaHttpRequest(request, serverlessOptions);
14300
+ try {
14301
+ await this.writeLambdaResponse(response, responseStream);
14302
+ } finally {
14303
+ await cleanup();
14304
+ }
14305
+ });
14263
14306
  }
14264
14307
  /**
14265
14308
  * Close a specific transport session.
@@ -14285,6 +14328,97 @@ var App = class {
14285
14328
  getCallerDir() {
14286
14329
  return this.callerDir;
14287
14330
  }
14331
+ lambdaEventToRequest(event) {
14332
+ const headers = new Headers();
14333
+ for (const [key, value] of Object.entries(event.headers ?? {})) {
14334
+ if (value !== void 0) {
14335
+ headers.set(key, value);
14336
+ }
14337
+ }
14338
+ if (event.cookies?.length) {
14339
+ headers.set("cookie", event.cookies.join("; "));
14340
+ }
14341
+ const method = event.requestContext?.http?.method || "GET";
14342
+ const scheme = headers.get("x-forwarded-proto") || "https";
14343
+ const host = headers.get("host") || event.requestContext?.domainName || "localhost";
14344
+ const rawPath = event.rawPath || "/mcp";
14345
+ const query = event.rawQueryString ? `?${event.rawQueryString}` : "";
14346
+ const url = `${scheme}://${host}${rawPath}${query}`;
14347
+ const hasBody = event.body !== void 0 && event.body !== null && method !== "GET" && method !== "HEAD";
14348
+ const body = hasBody ? Buffer.from(event.body, event.isBase64Encoded ? "base64" : "utf8") : void 0;
14349
+ return new Request(url, {
14350
+ method,
14351
+ headers,
14352
+ body
14353
+ });
14354
+ }
14355
+ async handleLambdaHttpRequest(request, options) {
14356
+ const transport = new WebStandardStreamableHTTPServerTransport({
14357
+ sessionIdGenerator: void 0,
14358
+ enableJsonResponse: options.enableJsonResponse ?? false
14359
+ });
14360
+ const server = this.createMcpServer({ serverless: options });
14361
+ await server.connect(transport);
14362
+ setCurrentServer(server);
14363
+ return {
14364
+ response: await transport.handleRequest(request),
14365
+ cleanup: async () => {
14366
+ await server.close().catch(() => {
14367
+ });
14368
+ }
14369
+ };
14370
+ }
14371
+ async writeLambdaResponse(response, responseStream) {
14372
+ const headers = {};
14373
+ response.headers.forEach((value, key) => {
14374
+ if (key.toLowerCase() !== "set-cookie") {
14375
+ headers[key] = value;
14376
+ }
14377
+ });
14378
+ const getSetCookie = response.headers.getSetCookie;
14379
+ const cookies = typeof getSetCookie === "function" ? getSetCookie.call(response.headers) : void 0;
14380
+ const writable = awslambda.HttpResponseStream.from(responseStream, {
14381
+ statusCode: response.status,
14382
+ headers,
14383
+ ...cookies && cookies.length > 0 ? { cookies } : {}
14384
+ });
14385
+ if (!response.body) {
14386
+ writable.end();
14387
+ return;
14388
+ }
14389
+ await pipeline(Readable2.fromWeb(response.body), writable);
14390
+ }
14391
+ isStatelessTransport(options) {
14392
+ return options?.serverless?.transportMode === "stateless";
14393
+ }
14394
+ getStateAdapter(options) {
14395
+ return options?.serverless?.stateAdapter;
14396
+ }
14397
+ async loadInstanceState(instanceId, options) {
14398
+ const adapter = this.getStateAdapter(options);
14399
+ if (adapter) {
14400
+ return adapter.get(instanceId);
14401
+ }
14402
+ return this.instanceState.get(instanceId);
14403
+ }
14404
+ async persistInstanceState(instanceId, state, options) {
14405
+ const adapter = this.getStateAdapter(options);
14406
+ if (adapter) {
14407
+ await adapter.set(instanceId, state);
14408
+ return;
14409
+ }
14410
+ this.instanceState.set(instanceId, state);
14411
+ }
14412
+ deleteInstanceState(instanceId, options) {
14413
+ const adapter = this.getStateAdapter(options);
14414
+ if (adapter) {
14415
+ void adapter.delete(instanceId).catch((error) => {
14416
+ console.error(`[MCP] Failed to delete state for ${instanceId}:`, error);
14417
+ });
14418
+ return;
14419
+ }
14420
+ this.instanceState.delete(instanceId);
14421
+ }
14288
14422
  // ==========================================================================
14289
14423
  // Public API: MCP Request Handling (for testing)
14290
14424
  // ==========================================================================
@@ -14503,7 +14637,7 @@ var App = class {
14503
14637
  // ==========================================================================
14504
14638
  // Private: Express Server
14505
14639
  // ==========================================================================
14506
- createExpressApp() {
14640
+ createExpressApp(options) {
14507
14641
  const app = express();
14508
14642
  app.use(express.json({ limit: "50mb" }));
14509
14643
  if (this.config.cors !== false) {
@@ -14531,12 +14665,12 @@ var App = class {
14531
14665
  websockets: this.instanceWebSockets.size
14532
14666
  });
14533
14667
  });
14534
- app.post("/mcp", (req, res) => this.handleMcpPost(req, res));
14535
- app.get("/mcp", (req, res) => this.handleMcpGet(req, res));
14536
- app.delete("/mcp", (req, res) => this.handleMcpDelete(req, res));
14668
+ app.post("/mcp", (req, res) => this.handleMcpPost(req, res, options));
14669
+ app.get("/mcp", (req, res) => this.handleMcpGet(req, res, options));
14670
+ app.delete("/mcp", (req, res) => this.handleMcpDelete(req, res, options));
14537
14671
  return app;
14538
14672
  }
14539
- async handleMcpPost(req, res) {
14673
+ async handleMcpPost(req, res, options) {
14540
14674
  const transportSessionId = req.headers["mcp-session-id"];
14541
14675
  try {
14542
14676
  if (req.body?.method === "tools/list" && this.tools.size === 0) {
@@ -14548,6 +14682,14 @@ var App = class {
14548
14682
  return;
14549
14683
  }
14550
14684
  let transport;
14685
+ if (this.isStatelessTransport(options)) {
14686
+ transport = this.createTransport(options);
14687
+ const server = this.createMcpServer(options);
14688
+ await server.connect(transport);
14689
+ setCurrentServer(server);
14690
+ await transport.handleRequest(req, res, req.body);
14691
+ return;
14692
+ }
14551
14693
  if (transportSessionId && this.transports.has(transportSessionId)) {
14552
14694
  transport = this.transports.get(transportSessionId);
14553
14695
  } else if (!transportSessionId && isInitializeRequest2(req.body)) {
@@ -14562,8 +14704,8 @@ var App = class {
14562
14704
  this.clientType = "unknown";
14563
14705
  }
14564
14706
  console.log(`[MCP] Client: ${clientName}, type: ${this.clientType}`);
14565
- transport = this.createTransport();
14566
- const server = this.createMcpServer();
14707
+ transport = this.createTransport(options);
14708
+ const server = this.createMcpServer(options);
14567
14709
  await server.connect(transport);
14568
14710
  this.serverByTransport.set(transport, server);
14569
14711
  setCurrentServer(server);
@@ -14589,8 +14731,16 @@ var App = class {
14589
14731
  }
14590
14732
  }
14591
14733
  }
14592
- async handleMcpGet(req, res) {
14734
+ async handleMcpGet(req, res, options) {
14593
14735
  const transportSessionId = req.headers["mcp-session-id"];
14736
+ if (this.isStatelessTransport(options)) {
14737
+ const transport2 = this.createTransport(options);
14738
+ const server = this.createMcpServer(options);
14739
+ await server.connect(transport2);
14740
+ setCurrentServer(server);
14741
+ await transport2.handleRequest(req, res);
14742
+ return;
14743
+ }
14594
14744
  if (!transportSessionId || !this.transports.has(transportSessionId)) {
14595
14745
  res.status(400).send("Invalid or missing transport session ID");
14596
14746
  return;
@@ -14598,8 +14748,16 @@ var App = class {
14598
14748
  const transport = this.transports.get(transportSessionId);
14599
14749
  await transport.handleRequest(req, res);
14600
14750
  }
14601
- async handleMcpDelete(req, res) {
14751
+ async handleMcpDelete(req, res, options) {
14602
14752
  const transportSessionId = req.headers["mcp-session-id"];
14753
+ if (this.isStatelessTransport(options)) {
14754
+ const transport2 = this.createTransport(options);
14755
+ const server = this.createMcpServer(options);
14756
+ await server.connect(transport2);
14757
+ setCurrentServer(server);
14758
+ await transport2.handleRequest(req, res);
14759
+ return;
14760
+ }
14603
14761
  if (!transportSessionId || !this.transports.has(transportSessionId)) {
14604
14762
  res.status(400).send("Invalid or missing transport session ID");
14605
14763
  return;
@@ -14610,10 +14768,12 @@ var App = class {
14610
14768
  // ==========================================================================
14611
14769
  // Private: MCP Server & Transport
14612
14770
  // ==========================================================================
14613
- createTransport() {
14771
+ createTransport(options) {
14772
+ const stateless = this.isStatelessTransport(options);
14614
14773
  const transport = new StreamableHTTPServerTransport({
14615
- sessionIdGenerator: () => randomUUID(),
14616
- onsessioninitialized: (newSessionId) => {
14774
+ sessionIdGenerator: stateless ? void 0 : (() => randomUUID()),
14775
+ enableJsonResponse: options?.serverless?.enableJsonResponse ?? false,
14776
+ onsessioninitialized: stateless ? void 0 : (newSessionId) => {
14617
14777
  this.transports.set(newSessionId, transport);
14618
14778
  console.log(`[MCP] Transport session created: ${newSessionId}`);
14619
14779
  this.config.onTransportSessionCreated?.({
@@ -14643,7 +14803,7 @@ var App = class {
14643
14803
  };
14644
14804
  return transport;
14645
14805
  }
14646
- createMcpServer() {
14806
+ createMcpServer(options) {
14647
14807
  const server = new McpServer(
14648
14808
  {
14649
14809
  name: this.config.name,
@@ -14653,7 +14813,7 @@ var App = class {
14653
14813
  { capabilities: { logging: {}, tools: {}, resources: {} } }
14654
14814
  );
14655
14815
  this.registerResources(server);
14656
- this.registerTools(server);
14816
+ this.registerTools(server, options);
14657
14817
  return server;
14658
14818
  }
14659
14819
  registerResources(server) {
@@ -14739,7 +14899,7 @@ var App = class {
14739
14899
  );
14740
14900
  }
14741
14901
  }
14742
- registerTools(server) {
14902
+ registerTools(server, options) {
14743
14903
  for (const [name, { config, handler }] of this.tools) {
14744
14904
  const toolMeta = this.buildToolMeta(config);
14745
14905
  const baseSchema = config.input || z3.object({});
@@ -14756,9 +14916,10 @@ var App = class {
14756
14916
  },
14757
14917
  async (args) => {
14758
14918
  let startMs = Date.now();
14919
+ let instanceId;
14920
+ let stateDirty = false;
14759
14921
  try {
14760
14922
  const input = config.input ? config.input.parse(args) : args;
14761
- let instanceId;
14762
14923
  if (hasUi && config.ui) {
14763
14924
  instanceId = this.resolveInstanceId(args._instanceId);
14764
14925
  }
@@ -14766,15 +14927,21 @@ var App = class {
14766
14927
  const hasWebSocket = resource?.config.experimental?.websocket === true;
14767
14928
  let ws;
14768
14929
  let websocketUrl;
14769
- if (hasWebSocket && instanceId) {
14930
+ if (hasWebSocket && instanceId && !options?.serverless) {
14770
14931
  ws = this.getOrCreateWebSocket(instanceId);
14771
14932
  websocketUrl = ws?.websocketUrl;
14772
14933
  }
14934
+ let currentState = instanceId ? await this.loadInstanceState(instanceId, options) : void 0;
14935
+ if (instanceId && currentState !== void 0) {
14936
+ this.instanceState.set(instanceId, currentState);
14937
+ }
14773
14938
  const context = {
14774
14939
  instanceId: instanceId || "",
14775
- getState: () => instanceId ? this.instanceState.get(instanceId) : void 0,
14940
+ getState: () => currentState,
14776
14941
  setState: (state) => {
14777
14942
  if (instanceId) {
14943
+ currentState = state;
14944
+ stateDirty = true;
14778
14945
  this.instanceState.set(instanceId, state);
14779
14946
  }
14780
14947
  },
@@ -14805,6 +14972,9 @@ var App = class {
14805
14972
  startMs = Date.now();
14806
14973
  const result = await handler(input, context);
14807
14974
  const durationMs = Date.now() - startMs;
14975
+ if (instanceId && stateDirty) {
14976
+ await this.persistInstanceState(instanceId, currentState, options);
14977
+ }
14808
14978
  try {
14809
14979
  this.config.onAfterToolCall?.({ toolName: name, args, result, durationMs, isError: false });
14810
14980
  } catch {
@@ -14818,6 +14988,9 @@ var App = class {
14818
14988
  const durationMs = Date.now() - startMs;
14819
14989
  console.error(`[MCP] Tool "${name}" failed:`, err.message);
14820
14990
  this.config.onToolError?.(name, err, args);
14991
+ if (instanceId && stateDirty) {
14992
+ await this.persistInstanceState(instanceId, this.instanceState.get(instanceId), options);
14993
+ }
14821
14994
  try {
14822
14995
  this.config.onAfterToolCall?.({ toolName: name, args, durationMs, isError: true, error: err });
14823
14996
  } catch {