@creature-ai/sdk 0.1.1 → 0.1.3

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.
@@ -93,12 +93,12 @@ var require_code = __commonJS({
93
93
  exports._ = _;
94
94
  var plus = new _Code("+");
95
95
  function str(strs, ...args) {
96
- const expr = [safeStringify2(strs[0])];
96
+ const expr = [safeStringify(strs[0])];
97
97
  let i = 0;
98
98
  while (i < args.length) {
99
99
  expr.push(plus);
100
100
  addCodeArg(expr, args[i]);
101
- expr.push(plus, safeStringify2(strs[++i]));
101
+ expr.push(plus, safeStringify(strs[++i]));
102
102
  }
103
103
  optimize(expr);
104
104
  return new _Code(expr);
@@ -150,16 +150,16 @@ var require_code = __commonJS({
150
150
  }
151
151
  exports.strConcat = strConcat;
152
152
  function interpolate(x) {
153
- return typeof x == "number" || typeof x == "boolean" || x === null ? x : safeStringify2(Array.isArray(x) ? x.join(",") : x);
153
+ return typeof x == "number" || typeof x == "boolean" || x === null ? x : safeStringify(Array.isArray(x) ? x.join(",") : x);
154
154
  }
155
155
  function stringify(x) {
156
- return new _Code(safeStringify2(x));
156
+ return new _Code(safeStringify(x));
157
157
  }
158
158
  exports.stringify = stringify;
159
- function safeStringify2(x) {
159
+ function safeStringify(x) {
160
160
  return JSON.stringify(x).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
161
161
  }
162
- exports.safeStringify = safeStringify2;
162
+ exports.safeStringify = safeStringify;
163
163
  function getProperty(key) {
164
164
  return typeof key == "string" && exports.IDENTIFIER.test(key) ? new _Code(`.${key}`) : _`[${key}]`;
165
165
  }
@@ -13563,9 +13563,9 @@ function isInitializeRequest2(body) {
13563
13563
  return msg.method === "initialize" && msg.jsonrpc === "2.0";
13564
13564
  }
13565
13565
 
13566
- // src/server/channel.ts
13566
+ // src/server/websocket.ts
13567
13567
  import { WebSocketServer, WebSocket } from "ws";
13568
- var AppSessionChannelInternal = class {
13568
+ var WebSocketConnectionInternal = class {
13569
13569
  constructor(id, config) {
13570
13570
  this.id = id;
13571
13571
  this.clientSchema = config.client;
@@ -13596,7 +13596,7 @@ var AppSessionChannelInternal = class {
13596
13596
  const message = this.clientSchema ? this.clientSchema.parse(raw) : raw;
13597
13597
  this.handler(message);
13598
13598
  } catch (error) {
13599
- console.error(`[Channel ${this.id}] Invalid message:`, error);
13599
+ console.error(`[WebSocket ${this.id}] Invalid message:`, error);
13600
13600
  }
13601
13601
  }
13602
13602
  broadcast(message) {
@@ -13610,14 +13610,14 @@ var AppSessionChannelInternal = class {
13610
13610
  }
13611
13611
  closeAll() {
13612
13612
  for (const client of this.clients) {
13613
- client.close(1e3, "Channel closed");
13613
+ client.close(1e3, "WebSocket closed");
13614
13614
  }
13615
13615
  this.clients.clear();
13616
13616
  }
13617
13617
  };
13618
- var ChannelManager = class {
13618
+ var WebSocketManager = class {
13619
13619
  wss = null;
13620
- channels = /* @__PURE__ */ new Map();
13620
+ connections = /* @__PURE__ */ new Map();
13621
13621
  /**
13622
13622
  * Attach the WebSocket server to an HTTP server.
13623
13623
  */
@@ -13625,7 +13625,7 @@ var ChannelManager = class {
13625
13625
  this.wss = new WebSocketServer({ noServer: true });
13626
13626
  server.on("upgrade", (request, socket, head) => {
13627
13627
  const url = request.url || "";
13628
- if (!url.startsWith("/channels/")) {
13628
+ if (!url.startsWith("/ws/")) {
13629
13629
  socket.destroy();
13630
13630
  return;
13631
13631
  }
@@ -13634,62 +13634,59 @@ var ChannelManager = class {
13634
13634
  });
13635
13635
  });
13636
13636
  this.wss.on("connection", (ws, req) => {
13637
- const match = req.url?.match(/^\/channels\/([^/]+)\/(.+)$/);
13637
+ const match = req.url?.match(/^\/ws\/(.+)$/);
13638
13638
  if (!match) {
13639
- ws.close(4e3, "Invalid channel URL");
13639
+ ws.close(4e3, "Invalid WebSocket URL");
13640
13640
  return;
13641
13641
  }
13642
- const [, channelName, appSessionId] = match;
13643
- const fullId = `${channelName}/${appSessionId}`;
13644
- const channel = this.channels.get(fullId);
13645
- if (!channel) {
13646
- ws.close(4004, "Channel not found");
13642
+ const [, instanceId] = match;
13643
+ const connection = this.connections.get(instanceId);
13644
+ if (!connection) {
13645
+ ws.close(4004, "Instance not found");
13647
13646
  return;
13648
13647
  }
13649
- channel.addClient(ws);
13648
+ connection.addClient(ws);
13650
13649
  ws.on("message", (data) => {
13651
13650
  try {
13652
13651
  const message = JSON.parse(data.toString());
13653
- channel.handleMessage(message);
13652
+ connection.handleMessage(message);
13654
13653
  } catch (e) {
13655
- console.error(`[Channel] Failed to parse message:`, e);
13654
+ console.error(`[WebSocket] Failed to parse message:`, e);
13656
13655
  }
13657
13656
  });
13658
13657
  ws.on("close", () => {
13659
- channel.removeClient(ws);
13658
+ connection.removeClient(ws);
13660
13659
  });
13661
13660
  ws.on("error", (error) => {
13662
- console.error(`[Channel ${fullId}] WebSocket error:`, error);
13663
- channel.removeClient(ws);
13661
+ console.error(`[WebSocket ${instanceId}] Error:`, error);
13662
+ connection.removeClient(ws);
13664
13663
  });
13665
13664
  });
13666
13665
  }
13667
13666
  /**
13668
- * Create a new channel for an AppSession.
13667
+ * Create a WebSocket connection for an instance.
13669
13668
  *
13670
- * @param channelName - The channel name (e.g., "updates", "terminal")
13671
- * @param appSessionId - The AppSession ID this channel belongs to
13672
- * @param config - Channel configuration
13669
+ * @param instanceId - The instance ID
13670
+ * @param config - WebSocket configuration
13673
13671
  * @param port - Server port for WebSocket URL
13674
- * @returns An AppSessionChannel for bidirectional communication
13672
+ * @returns A WebSocketConnection for bidirectional communication
13675
13673
  */
13676
- createChannel(channelName, appSessionId, config, port) {
13677
- const fullId = `${channelName}/${appSessionId}`;
13678
- const existing = this.channels.get(fullId);
13674
+ createWebSocket(instanceId, config, port) {
13675
+ const existing = this.connections.get(instanceId);
13679
13676
  if (existing) {
13680
13677
  existing.closeAll();
13681
13678
  }
13682
- const internal = new AppSessionChannelInternal(fullId, config);
13683
- this.channels.set(fullId, internal);
13679
+ const internal = new WebSocketConnectionInternal(instanceId, config);
13680
+ this.connections.set(instanceId, internal);
13684
13681
  return {
13685
- appSessionId,
13686
- url: `ws://localhost:${port}/channels/${channelName}/${appSessionId}`,
13682
+ instanceId,
13683
+ websocketUrl: `ws://localhost:${port}/ws/${instanceId}`,
13687
13684
  send: (msg) => internal.broadcast(msg),
13688
13685
  onMessage: (handler) => internal.setHandler(handler),
13689
13686
  onConnect: (handler) => internal.setConnectHandler(handler),
13690
13687
  close: () => {
13691
13688
  internal.closeAll();
13692
- this.channels.delete(fullId);
13689
+ this.connections.delete(instanceId);
13693
13690
  },
13694
13691
  get clientCount() {
13695
13692
  return internal.clientCount;
@@ -13697,228 +13694,49 @@ var ChannelManager = class {
13697
13694
  };
13698
13695
  }
13699
13696
  /**
13700
- * Check if a channel exists for an AppSession.
13697
+ * Check if a WebSocket exists for an instance.
13701
13698
  */
13702
- hasChannel(channelName, appSessionId) {
13703
- return this.channels.has(`${channelName}/${appSessionId}`);
13699
+ hasWebSocket(instanceId) {
13700
+ return this.connections.has(instanceId);
13704
13701
  }
13705
13702
  /**
13706
- * Check if any channels exist.
13703
+ * Close a specific instance's WebSocket.
13707
13704
  */
13708
- hasAnyChannels() {
13709
- return this.channels.size > 0;
13705
+ closeWebSocket(instanceId) {
13706
+ const connection = this.connections.get(instanceId);
13707
+ if (connection) {
13708
+ connection.closeAll();
13709
+ this.connections.delete(instanceId);
13710
+ return true;
13711
+ }
13712
+ return false;
13710
13713
  }
13711
13714
  /**
13712
- * Close all channels and shut down the WebSocket server.
13715
+ * Close all WebSocket connections and shut down the server.
13713
13716
  */
13714
13717
  closeAll() {
13715
- for (const channel of this.channels.values()) {
13716
- channel.closeAll();
13718
+ for (const connection of this.connections.values()) {
13719
+ connection.closeAll();
13717
13720
  }
13718
- this.channels.clear();
13721
+ this.connections.clear();
13719
13722
  if (this.wss) {
13720
13723
  this.wss.close();
13721
13724
  this.wss = null;
13722
13725
  }
13723
13726
  }
13724
13727
  };
13725
- var ChannelDefinition = class {
13726
- constructor(manager, name, config, getPort) {
13727
- this.manager = manager;
13728
- this.name = name;
13729
- this.config = config;
13730
- this.getPort = getPort;
13731
- }
13732
- /**
13733
- * Create a channel for a specific AppSession.
13734
- *
13735
- * @param appSessionId - The AppSession ID (e.g., from a terminal AppSession)
13736
- * @returns An AppSessionChannel for bidirectional communication
13737
- */
13738
- forAppSession(appSessionId) {
13739
- return this.manager.createChannel(this.name, appSessionId, this.config, this.getPort());
13740
- }
13741
- /**
13742
- * Check if a channel exists for an AppSession.
13743
- */
13744
- hasChannel(appSessionId) {
13745
- return this.manager.hasChannel(this.name, appSessionId);
13746
- }
13747
- /**
13748
- * Get the channel name.
13749
- */
13750
- get channelName() {
13751
- return this.name;
13752
- }
13753
- };
13754
-
13755
- // src/server/appSession.ts
13756
- var ServerAppSession = class {
13757
- id;
13758
- _state;
13759
- listeners = /* @__PURE__ */ new Set();
13760
- _channel = null;
13761
- constructor(initialState = {}, options = {}) {
13762
- this.id = options.id ?? this.generateId();
13763
- this._state = {
13764
- internal: initialState.internal ?? {},
13765
- backend: initialState.backend ?? {},
13766
- ui: initialState.ui ?? null
13767
- };
13768
- console.log(`[AppSession] Created: ${this.id}`);
13769
- }
13770
- generateId() {
13771
- return `appSession_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
13772
- }
13773
- get state() {
13774
- return this._state;
13775
- }
13776
- get internal() {
13777
- return this._state.internal;
13778
- }
13779
- get backend() {
13780
- return this._state.backend;
13781
- }
13782
- get ui() {
13783
- return this._state.ui;
13784
- }
13785
- get channel() {
13786
- return this._channel;
13787
- }
13788
- get channelUrl() {
13789
- return this._channel?.url;
13790
- }
13791
- setChannel(channel) {
13792
- console.log(`[AppSession:${this.id}] Channel attached: ${channel.url}`);
13793
- this._channel = channel;
13794
- }
13795
- subscribe(listener) {
13796
- this.listeners.add(listener);
13797
- return () => {
13798
- this.listeners.delete(listener);
13799
- };
13800
- }
13801
- notify(prevState) {
13802
- this.listeners.forEach((listener) => listener(this._state, prevState));
13803
- }
13804
- setState(partial) {
13805
- const prev = this._state;
13806
- this._state = {
13807
- internal: partial.internal !== void 0 ? { ...prev.internal, ...partial.internal } : prev.internal,
13808
- backend: partial.backend !== void 0 ? { ...prev.backend, ...partial.backend } : prev.backend,
13809
- ui: partial.ui !== void 0 ? partial.ui : prev.ui
13810
- };
13811
- this.notify(prev);
13812
- }
13813
- setInternal(internal) {
13814
- const prev = this._state;
13815
- this._state = {
13816
- ...prev,
13817
- internal: { ...prev.internal, ...internal }
13818
- };
13819
- this.notify(prev);
13820
- }
13821
- setBackend(backend) {
13822
- const prev = this._state;
13823
- this._state = {
13824
- ...prev,
13825
- backend: { ...prev.backend, ...backend }
13826
- };
13827
- this.notify(prev);
13828
- }
13829
- setUi(ui) {
13830
- const prev = this._state;
13831
- this._state = { ...prev, ui };
13832
- this.notify(prev);
13833
- }
13834
- updateUi(partial) {
13835
- const prev = this._state;
13836
- const newUi = prev.ui !== null ? { ...prev.ui, ...partial } : partial;
13837
- this._state = { ...prev, ui: newUi };
13838
- this.notify(prev);
13839
- }
13840
- injectAppSessionId(data) {
13841
- return { ...data, appSessionId: this.id };
13842
- }
13843
- close() {
13844
- console.log(`[AppSession:${this.id}] Closing`);
13845
- this._channel?.close();
13846
- this._channel = null;
13847
- this.listeners.clear();
13848
- }
13849
- };
13850
- var AppSessionManager = class {
13851
- appSessions = /* @__PURE__ */ new Map();
13852
- channelManager = null;
13853
- getPort;
13854
- appSessionChannelName = "app-session";
13855
- constructor(config) {
13856
- this.channelManager = config.channelManager ?? null;
13857
- this.getPort = config.getPort;
13858
- }
13859
- setChannelManager(channelManager) {
13860
- this.channelManager = channelManager;
13861
- }
13862
- create(initialState = {}, options = {}) {
13863
- console.log(`[AppSessionManager] Creating AppSession`, { websockets: options.websockets ?? false });
13864
- const appSession = new ServerAppSession(initialState, { id: options.id });
13865
- if (options.websockets && this.channelManager) {
13866
- const channel = this.channelManager.createChannel(
13867
- this.appSessionChannelName,
13868
- appSession.id,
13869
- {},
13870
- this.getPort()
13871
- );
13872
- appSession.setChannel(channel);
13873
- }
13874
- this.appSessions.set(appSession.id, appSession);
13875
- console.log(`[AppSessionManager] AppSession registered: ${appSession.id} (total: ${this.appSessions.size})`);
13876
- return appSession;
13877
- }
13878
- get(id) {
13879
- return this.appSessions.get(id);
13880
- }
13881
- require(id) {
13882
- const appSession = this.appSessions.get(id);
13883
- if (!appSession) {
13884
- throw new Error(`AppSession not found: ${id}`);
13885
- }
13886
- return appSession;
13887
- }
13888
- has(id) {
13889
- return this.appSessions.has(id);
13890
- }
13891
- delete(id) {
13892
- const appSession = this.appSessions.get(id);
13893
- if (appSession) {
13894
- console.log(`[AppSessionManager] Deleting AppSession: ${id}`);
13895
- appSession.close();
13896
- this.appSessions.delete(id);
13897
- return true;
13898
- }
13899
- console.log(`[AppSessionManager] AppSession not found for deletion: ${id}`);
13900
- return false;
13901
- }
13902
- closeAll() {
13903
- for (const appSession of this.appSessions.values()) {
13904
- appSession.close();
13905
- }
13906
- this.appSessions.clear();
13907
- }
13908
- get size() {
13909
- return this.appSessions.size;
13910
- }
13911
- };
13912
13728
 
13913
13729
  // src/server/app.ts
13914
13730
  var App = class {
13731
+ // ==========================================================================
13732
+ // Private Properties
13733
+ // ==========================================================================
13915
13734
  config;
13916
13735
  tools = /* @__PURE__ */ new Map();
13917
13736
  resources = /* @__PURE__ */ new Map();
13918
13737
  transports = /* @__PURE__ */ new Map();
13919
- channelManager = new ChannelManager();
13920
- channels = /* @__PURE__ */ new Map();
13921
- appSessionManager;
13738
+ websocketManager = new WebSocketManager();
13739
+ instanceWebSockets = /* @__PURE__ */ new Map();
13922
13740
  httpServer = null;
13923
13741
  isDev;
13924
13742
  hmrPort = null;
@@ -13928,32 +13746,211 @@ var App = class {
13928
13746
  isShuttingDown = false;
13929
13747
  resourceCache = /* @__PURE__ */ new Map();
13930
13748
  resourceCacheConfig;
13749
+ /** Server-side instance state, keyed by instanceId. */
13750
+ instanceState = /* @__PURE__ */ new Map();
13751
+ /** Callbacks to invoke when an instance is destroyed. */
13752
+ instanceDestroyCallbacks = [];
13753
+ /** Singleton instanceIds per resourceUri (for non-multiInstance resources). */
13754
+ singletonInstances = /* @__PURE__ */ new Map();
13755
+ /** Whether the connected host supports multiInstance. ChatGPT doesn't, Creature does. */
13756
+ hostSupportsMultiInstance = false;
13757
+ // ==========================================================================
13758
+ // Constructor
13759
+ // ==========================================================================
13931
13760
  constructor(config, callerDir) {
13932
13761
  this.callerDir = callerDir || process.cwd();
13933
13762
  this.config = config;
13934
13763
  this.isDev = config.dev ?? process.env.NODE_ENV === "development";
13935
- this.appSessionManager = new AppSessionManager({
13936
- channelManager: this.channelManager,
13937
- getPort: () => this.getPort()
13938
- });
13939
13764
  this.resourceCacheConfig = {
13940
13765
  maxSize: config.resourceCache?.maxSize ?? 100,
13941
13766
  ttlMs: config.resourceCache?.ttlMs ?? 0,
13942
13767
  enabled: config.resourceCache?.enabled ?? true
13943
13768
  };
13944
- if (this.isDev) {
13945
- if (config.hmrPort) {
13946
- this.hmrPort = config.hmrPort;
13769
+ if (this.isDev && config.hmrPort) {
13770
+ this.hmrPort = config.hmrPort;
13771
+ }
13772
+ }
13773
+ // ==========================================================================
13774
+ // Public API: Registration
13775
+ // ==========================================================================
13776
+ /**
13777
+ * Define a UI resource.
13778
+ */
13779
+ resource(config) {
13780
+ if (!config.uri.startsWith("ui://")) {
13781
+ throw new Error(`Resource URI must start with "ui://": ${config.uri}`);
13782
+ }
13783
+ const normalizedConfig = {
13784
+ ...config,
13785
+ html: typeof config.html === "string" ? htmlLoader(config.html, this.getCallerDir()) : config.html
13786
+ };
13787
+ this.resources.set(config.uri, { config: normalizedConfig });
13788
+ return this;
13789
+ }
13790
+ /**
13791
+ * Define a tool.
13792
+ */
13793
+ tool(name, config, handler) {
13794
+ this.tools.set(name, {
13795
+ config,
13796
+ handler
13797
+ });
13798
+ return this;
13799
+ }
13800
+ // ==========================================================================
13801
+ // Public API: Server Lifecycle
13802
+ // ==========================================================================
13803
+ /**
13804
+ * Start the MCP server.
13805
+ */
13806
+ async start() {
13807
+ const port = this.getPort();
13808
+ const expressApp = this.createExpressApp();
13809
+ this.httpServer = expressApp.listen(port, () => {
13810
+ console.log(`MCP server ready on port ${port}`);
13811
+ });
13812
+ this.websocketManager.attach(this.httpServer);
13813
+ console.log(`WebSocket available at ws://localhost:${port}/ws`);
13814
+ this.registerShutdownHandlers();
13815
+ }
13816
+ /**
13817
+ * Stop the MCP server gracefully.
13818
+ */
13819
+ async stop() {
13820
+ const gracefulTimeout = this.config.gracefulShutdownTimeout ?? 5e3;
13821
+ const keepAliveTimeout = this.config.keepAliveTimeout ?? 5e3;
13822
+ try {
13823
+ await this.config.onShutdown?.();
13824
+ } catch (error) {
13825
+ console.error("[MCP] Error in onShutdown callback:", error);
13826
+ }
13827
+ for (const instanceId of this.instanceState.keys()) {
13828
+ this.destroyInstance(instanceId);
13829
+ }
13830
+ this.websocketManager.closeAll();
13831
+ const transportClosePromises = [...this.transports.values()].map(
13832
+ (transport) => transport.close().catch(() => {
13833
+ })
13834
+ );
13835
+ await Promise.race([
13836
+ Promise.all(transportClosePromises),
13837
+ new Promise((resolve2) => setTimeout(resolve2, gracefulTimeout))
13838
+ ]);
13839
+ this.transports.clear();
13840
+ if (this.httpServer) {
13841
+ this.httpServer.keepAliveTimeout = keepAliveTimeout;
13842
+ await new Promise((resolve2) => {
13843
+ const forceClose = setTimeout(resolve2, gracefulTimeout);
13844
+ this.httpServer?.close(() => {
13845
+ clearTimeout(forceClose);
13846
+ resolve2();
13847
+ });
13848
+ });
13849
+ this.httpServer = null;
13850
+ }
13851
+ console.log("[MCP] Shutdown complete");
13852
+ }
13853
+ // ==========================================================================
13854
+ // Public API: Instance Lifecycle
13855
+ // ==========================================================================
13856
+ /**
13857
+ * Create an instance with optional WebSocket support.
13858
+ *
13859
+ * Most tools don't need this — the SDK creates instances automatically.
13860
+ * Only call createInstance() when you need a WebSocket URL for real-time updates.
13861
+ */
13862
+ createInstance(options = {}) {
13863
+ const instanceId = this.generateInstanceId();
13864
+ if (options.websocket) {
13865
+ const ws = this.websocketManager.createWebSocket(
13866
+ instanceId,
13867
+ {},
13868
+ this.getPort()
13869
+ );
13870
+ this.instanceWebSockets.set(instanceId, ws);
13871
+ return {
13872
+ instanceId,
13873
+ websocketUrl: ws.websocketUrl,
13874
+ send: ws.send,
13875
+ onMessage: ws.onMessage,
13876
+ onConnect: ws.onConnect
13877
+ };
13878
+ }
13879
+ return { instanceId };
13880
+ }
13881
+ /**
13882
+ * Register a callback to be invoked when an instance is destroyed.
13883
+ */
13884
+ onInstanceDestroy(callback) {
13885
+ this.instanceDestroyCallbacks.push(callback);
13886
+ }
13887
+ /**
13888
+ * Destroy an instance and clean up its resources.
13889
+ */
13890
+ destroyInstance(instanceId) {
13891
+ const state = this.instanceState.get(instanceId);
13892
+ const hasState = state !== void 0 || this.instanceState.has(instanceId);
13893
+ const hasWebSocket = this.instanceWebSockets.has(instanceId);
13894
+ if (hasState || hasWebSocket) {
13895
+ for (const callback of this.instanceDestroyCallbacks) {
13896
+ try {
13897
+ callback({ instanceId, state });
13898
+ } catch (error) {
13899
+ console.error(`[MCP] onInstanceDestroy callback error for ${instanceId}:`, error);
13900
+ }
13901
+ }
13902
+ const ws = this.instanceWebSockets.get(instanceId);
13903
+ if (ws) {
13904
+ ws.close();
13905
+ this.instanceWebSockets.delete(instanceId);
13947
13906
  }
13907
+ this.instanceState.delete(instanceId);
13908
+ console.log(`[MCP] Instance destroyed: ${instanceId}`);
13909
+ return true;
13910
+ }
13911
+ return false;
13912
+ }
13913
+ /**
13914
+ * Check if an instance exists.
13915
+ */
13916
+ hasInstance(instanceId) {
13917
+ return this.instanceState.has(instanceId);
13918
+ }
13919
+ /**
13920
+ * Get instance state.
13921
+ */
13922
+ getInstanceState(instanceId) {
13923
+ return this.instanceState.get(instanceId);
13924
+ }
13925
+ /**
13926
+ * Set instance state directly.
13927
+ */
13928
+ setInstanceState(instanceId, state) {
13929
+ this.instanceState.set(instanceId, state);
13930
+ }
13931
+ /**
13932
+ * Create a WebSocket for an existing instance.
13933
+ *
13934
+ * Use this when you need real-time communication for an instance
13935
+ * that was created by a tool handler (which provides instanceId via context).
13936
+ */
13937
+ createWebSocketForInstance(instanceId) {
13938
+ if (this.instanceWebSockets.has(instanceId)) {
13939
+ return this.instanceWebSockets.get(instanceId);
13948
13940
  }
13941
+ const ws = this.websocketManager.createWebSocket(
13942
+ instanceId,
13943
+ {},
13944
+ this.getPort()
13945
+ );
13946
+ this.instanceWebSockets.set(instanceId, ws);
13947
+ return ws;
13949
13948
  }
13950
- // ============================================================================
13951
- // Public API: Transport Session Lifecycle
13952
- // ============================================================================
13949
+ // ==========================================================================
13950
+ // Public API: Transport Sessions
13951
+ // ==========================================================================
13953
13952
  /**
13954
13953
  * Get list of active transport sessions.
13955
- * Transport sessions are MCP protocol connections (not AppSessions).
13956
- * Useful for monitoring and debugging connection state.
13957
13954
  */
13958
13955
  getTransportSessions() {
13959
13956
  return [...this.transports.keys()].map((id) => ({
@@ -13969,9 +13966,6 @@ var App = class {
13969
13966
  }
13970
13967
  /**
13971
13968
  * Close a specific transport session.
13972
- *
13973
- * @param sessionId - The transport session ID to close
13974
- * @returns true if the session was found and closed
13975
13969
  */
13976
13970
  closeTransportSession(sessionId) {
13977
13971
  const transport = this.transports.get(sessionId);
@@ -13983,28 +13977,23 @@ var App = class {
13983
13977
  }
13984
13978
  return false;
13985
13979
  }
13986
- // ============================================================================
13987
- // Public API: Resource Cache Management
13988
- // ============================================================================
13980
+ // ==========================================================================
13981
+ // Public API: Resource Cache
13982
+ // ==========================================================================
13989
13983
  /**
13990
13984
  * Clear all cached resource content.
13991
- * Use after updating UI files during development.
13992
13985
  */
13993
13986
  clearResourceCache() {
13994
13987
  this.resourceCache.clear();
13995
13988
  }
13996
13989
  /**
13997
13990
  * Clear a specific resource from the cache.
13998
- *
13999
- * @param uri - The resource URI to clear
14000
- * @returns true if the resource was in the cache and removed
14001
13991
  */
14002
13992
  clearResourceCacheEntry(uri) {
14003
13993
  return this.resourceCache.delete(uri);
14004
13994
  }
14005
13995
  /**
14006
13996
  * Get resource cache statistics.
14007
- * Useful for monitoring cache performance.
14008
13997
  */
14009
13998
  getResourceCacheStats() {
14010
13999
  return {
@@ -14013,9 +14002,84 @@ var App = class {
14013
14002
  enabled: this.resourceCacheConfig.enabled
14014
14003
  };
14015
14004
  }
14005
+ // ==========================================================================
14006
+ // Private: Configuration Helpers
14007
+ // ==========================================================================
14008
+ getPort() {
14009
+ return this.config.port || parseInt(process.env.MCP_PORT || process.env.PORT || "3000", 10);
14010
+ }
14011
+ getCallerDir() {
14012
+ return this.callerDir;
14013
+ }
14014
+ getHmrPort() {
14015
+ if (!this.isDev) return null;
14016
+ if (this.hmrPort !== null) return this.hmrPort;
14017
+ const hmrConfig = readHmrConfig();
14018
+ if (hmrConfig) {
14019
+ this.hmrPort = hmrConfig.port;
14020
+ }
14021
+ this.hmrConfigChecked = true;
14022
+ return this.hmrPort;
14023
+ }
14024
+ // ==========================================================================
14025
+ // Private: Instance Helpers
14026
+ // ==========================================================================
14027
+ generateInstanceId() {
14028
+ return `inst_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
14029
+ }
14030
+ /**
14031
+ * Resolve instanceId for a tool call based on resource configuration.
14032
+ *
14033
+ * - Singleton resources (default): Reuse same instanceId per resourceUri
14034
+ * - Multi-instance resources: Generate new instanceId (unless provided in input)
14035
+ *
14036
+ * @param resourceUri The resource URI from tool config
14037
+ * @param inputInstanceId instanceId from tool call input args (if any)
14038
+ */
14039
+ resolveInstanceId(resourceUri, inputInstanceId) {
14040
+ if (typeof inputInstanceId === "string") {
14041
+ return inputInstanceId;
14042
+ }
14043
+ const resource = this.resources.get(resourceUri);
14044
+ const isMultiInstance = resource?.config.multiInstance && this.hostSupportsMultiInstance;
14045
+ if (isMultiInstance) {
14046
+ return this.generateInstanceId();
14047
+ }
14048
+ let instanceId = this.singletonInstances.get(resourceUri);
14049
+ if (!instanceId) {
14050
+ instanceId = this.generateInstanceId();
14051
+ this.singletonInstances.set(resourceUri, instanceId);
14052
+ console.log(`[MCP] Singleton instance created for ${resourceUri}: ${instanceId}`);
14053
+ }
14054
+ return instanceId;
14055
+ }
14056
+ // ==========================================================================
14057
+ // Private: Schema Introspection
14058
+ // ==========================================================================
14059
+ /**
14060
+ * Extract the shape (properties) from a Zod schema.
14061
+ */
14062
+ getSchemaShape(schema) {
14063
+ if ("_def" in schema && typeof schema._def?.shape === "function") {
14064
+ return schema._def.shape();
14065
+ }
14066
+ return void 0;
14067
+ }
14016
14068
  /**
14017
- * Get a cached resource if valid, or null if not cached/expired.
14069
+ * Check if a field is required in a Zod schema.
14018
14070
  */
14071
+ isFieldRequired(schema, fieldName) {
14072
+ const shape = this.getSchemaShape(schema);
14073
+ if (!shape || !(fieldName in shape)) return false;
14074
+ const field = shape[fieldName];
14075
+ if ("_def" in field && field._def?.typeName === "ZodOptional") {
14076
+ return false;
14077
+ }
14078
+ return true;
14079
+ }
14080
+ // ==========================================================================
14081
+ // Private: Resource Cache
14082
+ // ==========================================================================
14019
14083
  getCachedResource(uri) {
14020
14084
  if (!this.resourceCacheConfig.enabled) return null;
14021
14085
  const cached = this.resourceCache.get(uri);
@@ -14027,9 +14091,6 @@ var App = class {
14027
14091
  }
14028
14092
  return cached.html;
14029
14093
  }
14030
- /**
14031
- * Store a resource in the cache with LRU eviction.
14032
- */
14033
14094
  setCachedResource(uri, html) {
14034
14095
  if (!this.resourceCacheConfig.enabled) return;
14035
14096
  const { maxSize } = this.resourceCacheConfig;
@@ -14041,81 +14102,136 @@ var App = class {
14041
14102
  }
14042
14103
  this.resourceCache.set(uri, { html, timestamp: Date.now() });
14043
14104
  }
14044
- /**
14045
- * Get the HMR port, reading from hmr.json lazily if not already set.
14046
- * This handles the timing issue where Vite may not have written hmr.json
14047
- * when the server first starts.
14048
- */
14049
- getHmrPort() {
14050
- if (!this.isDev) return null;
14051
- if (this.hmrPort !== null) return this.hmrPort;
14052
- const hmrConfig = readHmrConfig();
14053
- if (hmrConfig) {
14054
- this.hmrPort = hmrConfig.port;
14055
- }
14056
- this.hmrConfigChecked = true;
14057
- return this.hmrPort;
14105
+ // ==========================================================================
14106
+ // Private: Express Server
14107
+ // ==========================================================================
14108
+ createExpressApp() {
14109
+ const app = express();
14110
+ app.use(express.json());
14111
+ app.use((req, res, next) => {
14112
+ res.setHeader("Access-Control-Allow-Origin", "*");
14113
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
14114
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Mcp-Session-Id");
14115
+ res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
14116
+ if (req.method === "OPTIONS") {
14117
+ res.status(204).end();
14118
+ return;
14119
+ }
14120
+ next();
14121
+ });
14122
+ app.get("/health", (_req, res) => {
14123
+ res.json({
14124
+ status: "ok",
14125
+ server: this.config.name,
14126
+ version: this.config.version,
14127
+ activeSessions: this.transports.size,
14128
+ websockets: this.instanceWebSockets.size
14129
+ });
14130
+ });
14131
+ app.post("/mcp", (req, res) => this.handleMcpPost(req, res));
14132
+ app.get("/mcp", (req, res) => this.handleMcpGet(req, res));
14133
+ app.delete("/mcp", (req, res) => this.handleMcpDelete(req, res));
14134
+ return app;
14058
14135
  }
14059
- getCallerDir() {
14060
- return this.callerDir;
14136
+ async handleMcpPost(req, res) {
14137
+ const transportSessionId = req.headers["mcp-session-id"];
14138
+ try {
14139
+ let transport;
14140
+ if (transportSessionId && this.transports.has(transportSessionId)) {
14141
+ transport = this.transports.get(transportSessionId);
14142
+ } else if (!transportSessionId && isInitializeRequest2(req.body)) {
14143
+ const clientName = req.body?.params?.clientInfo?.name;
14144
+ this.hostSupportsMultiInstance = clientName === "creature";
14145
+ console.log(`[MCP] Client: ${clientName}, multiInstance support: ${this.hostSupportsMultiInstance}`);
14146
+ transport = this.createTransport();
14147
+ const server = this.createMcpServer();
14148
+ await server.connect(transport);
14149
+ await transport.handleRequest(req, res, req.body);
14150
+ return;
14151
+ } else {
14152
+ res.status(400).json({
14153
+ jsonrpc: "2.0",
14154
+ error: { code: -32e3, message: "Bad Request: No valid transport session ID" },
14155
+ id: null
14156
+ });
14157
+ return;
14158
+ }
14159
+ await transport.handleRequest(req, res, req.body);
14160
+ } catch (error) {
14161
+ console.error("Error:", error);
14162
+ if (!res.headersSent) {
14163
+ res.status(500).json({
14164
+ jsonrpc: "2.0",
14165
+ error: { code: -32603, message: "Internal server error" },
14166
+ id: null
14167
+ });
14168
+ }
14169
+ }
14061
14170
  }
14062
- /**
14063
- * Define a UI resource.
14064
- */
14065
- resource(config) {
14066
- if (!config.uri.startsWith("ui://")) {
14067
- throw new Error(`Resource URI must start with "ui://": ${config.uri}`);
14171
+ async handleMcpGet(req, res) {
14172
+ const transportSessionId = req.headers["mcp-session-id"];
14173
+ if (!transportSessionId || !this.transports.has(transportSessionId)) {
14174
+ res.status(400).send("Invalid or missing transport session ID");
14175
+ return;
14068
14176
  }
14069
- const normalizedConfig = {
14070
- ...config,
14071
- html: typeof config.html === "string" ? htmlLoader(config.html, this.getCallerDir()) : config.html
14072
- };
14073
- this.resources.set(config.uri, { config: normalizedConfig });
14074
- return this;
14177
+ const transport = this.transports.get(transportSessionId);
14178
+ await transport.handleRequest(req, res);
14075
14179
  }
14076
- tool(name, config, handler) {
14077
- this.tools.set(name, {
14078
- config,
14079
- handler
14180
+ async handleMcpDelete(req, res) {
14181
+ const transportSessionId = req.headers["mcp-session-id"];
14182
+ if (!transportSessionId || !this.transports.has(transportSessionId)) {
14183
+ res.status(400).send("Invalid or missing transport session ID");
14184
+ return;
14185
+ }
14186
+ const transport = this.transports.get(transportSessionId);
14187
+ await transport.handleRequest(req, res);
14188
+ }
14189
+ // ==========================================================================
14190
+ // Private: MCP Server & Transport
14191
+ // ==========================================================================
14192
+ createTransport() {
14193
+ const transport = new StreamableHTTPServerTransport({
14194
+ sessionIdGenerator: () => randomUUID(),
14195
+ onsessioninitialized: (newSessionId) => {
14196
+ this.transports.set(newSessionId, transport);
14197
+ console.log(`[MCP] Transport session created: ${newSessionId}`);
14198
+ this.config.onTransportSessionCreated?.({
14199
+ id: newSessionId,
14200
+ transport: "streamable-http"
14201
+ });
14202
+ }
14080
14203
  });
14081
- return this;
14082
- }
14083
- channel(name, config = {}) {
14084
- const definition = new ChannelDefinition(
14085
- this.channelManager,
14086
- name,
14087
- config,
14088
- () => this.getPort()
14089
- );
14090
- this.channels.set(name, definition);
14091
- return definition;
14092
- }
14093
- getPort() {
14094
- return this.config.port || parseInt(process.env.MCP_PORT || process.env.PORT || "3000", 10);
14095
- }
14096
- session(initialState = {}, options = {}) {
14097
- return this.appSessionManager.create(initialState, options);
14098
- }
14099
- getSession(id) {
14100
- return this.appSessionManager.get(id);
14101
- }
14102
- requireSession(id) {
14103
- return this.appSessionManager.require(id);
14104
- }
14105
- hasSession(id) {
14106
- return this.appSessionManager.has(id);
14107
- }
14108
- closeSession(id) {
14109
- return this.appSessionManager.delete(id);
14204
+ transport.onerror = (error) => {
14205
+ const sid = transport.sessionId || "unknown";
14206
+ console.error(`[MCP] Transport session error for ${sid}:`, error.message);
14207
+ this.config.onTransportSessionError?.(
14208
+ { id: sid, transport: "streamable-http" },
14209
+ error
14210
+ );
14211
+ };
14212
+ transport.onclose = () => {
14213
+ const sid = transport.sessionId;
14214
+ if (sid) {
14215
+ console.log(`[MCP] Transport session closed: ${sid}. Client should re-initialize to continue.`);
14216
+ this.transports.delete(sid);
14217
+ this.config.onTransportSessionClosed?.({
14218
+ id: sid,
14219
+ transport: "streamable-http"
14220
+ });
14221
+ }
14222
+ };
14223
+ return transport;
14110
14224
  }
14111
- /**
14112
- * Create an MCP server instance with all registered tools and resources.
14113
- */
14114
14225
  createMcpServer() {
14115
14226
  const server = new McpServer(
14116
14227
  { name: this.config.name, version: this.config.version },
14117
14228
  { capabilities: { logging: {} } }
14118
14229
  );
14230
+ this.registerResources(server);
14231
+ this.registerTools(server);
14232
+ return server;
14233
+ }
14234
+ registerResources(server) {
14119
14235
  for (const [uri, { config }] of this.resources) {
14120
14236
  server.registerResource(
14121
14237
  config.name,
@@ -14131,7 +14247,6 @@ var App = class {
14131
14247
  html = injectHmrClient(html, hmrPort);
14132
14248
  }
14133
14249
  const mcpAppsMeta = {
14134
- // MCP Apps Extension metadata
14135
14250
  ui: {
14136
14251
  displayModes: config.displayModes,
14137
14252
  ...config.icon && {
@@ -14142,7 +14257,6 @@ var App = class {
14142
14257
  },
14143
14258
  ...config.csp && { csp: config.csp }
14144
14259
  },
14145
- // ChatGPT Apps SDK metadata (also included in MCP Apps format)
14146
14260
  "openai/widgetPrefersBorder": true
14147
14261
  };
14148
14262
  const chatgptMeta = {
@@ -14150,14 +14264,12 @@ var App = class {
14150
14264
  };
14151
14265
  return {
14152
14266
  contents: [
14153
- // MCP Apps format (with both MCP and ChatGPT metadata)
14154
14267
  {
14155
14268
  uri,
14156
14269
  mimeType: MIME_TYPES.MCP_APPS,
14157
14270
  text: html,
14158
14271
  _meta: mcpAppsMeta
14159
14272
  },
14160
- // ChatGPT format (skybridge MIME type)
14161
14273
  {
14162
14274
  uri,
14163
14275
  mimeType: MIME_TYPES.CHATGPT,
@@ -14169,39 +14281,62 @@ var App = class {
14169
14281
  }
14170
14282
  );
14171
14283
  }
14284
+ }
14285
+ registerTools(server) {
14172
14286
  for (const [name, { config, handler }] of this.tools) {
14173
- const toolMeta = {};
14174
- if (config.ui) {
14175
- toolMeta.ui = {
14176
- resourceUri: config.ui,
14177
- visibility: config.visibility || ["model", "app"],
14178
- ...config.displayModes && { displayModes: config.displayModes },
14179
- ...config.defaultDisplayMode && {
14180
- defaultDisplayMode: config.defaultDisplayMode
14181
- },
14182
- ...config.multiSession && { multiSession: config.multiSession }
14183
- };
14184
- toolMeta["openai/outputTemplate"] = config.ui;
14185
- }
14186
- if (config.loadingMessage) {
14187
- toolMeta["openai/toolInvocation/invoking"] = config.loadingMessage;
14188
- }
14189
- if (config.completedMessage) {
14190
- toolMeta["openai/toolInvocation/invoked"] = config.completedMessage;
14191
- }
14287
+ const toolMeta = this.buildToolMeta(config);
14192
14288
  const inputSchema = config.input || z2.object({});
14289
+ const description = this.buildToolDescription(config, inputSchema);
14290
+ const hasUi = !!config.ui;
14193
14291
  server.registerTool(
14194
14292
  name,
14195
14293
  {
14196
- description: config.description,
14294
+ description,
14197
14295
  inputSchema,
14198
14296
  ...Object.keys(toolMeta).length > 0 && { _meta: toolMeta }
14199
14297
  },
14200
14298
  async (args) => {
14201
14299
  try {
14202
14300
  const input = config.input ? config.input.parse(args) : args;
14203
- const result = await handler(input, {});
14204
- return this.formatToolResult(result);
14301
+ let instanceId;
14302
+ if (hasUi && config.ui) {
14303
+ instanceId = this.resolveInstanceId(config.ui, args.instanceId);
14304
+ }
14305
+ const resource = config.ui ? this.resources.get(config.ui) : void 0;
14306
+ const hasWebSocket = resource?.config.websocket === true;
14307
+ let ws;
14308
+ let websocketUrl;
14309
+ if (hasWebSocket && instanceId) {
14310
+ ws = this.getOrCreateWebSocket(instanceId);
14311
+ websocketUrl = ws?.websocketUrl;
14312
+ }
14313
+ const context = {
14314
+ instanceId: instanceId || "",
14315
+ getState: () => instanceId ? this.instanceState.get(instanceId) : void 0,
14316
+ setState: (state) => {
14317
+ if (instanceId) {
14318
+ this.instanceState.set(instanceId, state);
14319
+ }
14320
+ },
14321
+ send: (message) => {
14322
+ if (ws) {
14323
+ ws.send(message);
14324
+ }
14325
+ },
14326
+ onMessage: (messageHandler) => {
14327
+ if (ws) {
14328
+ ws.onMessage(messageHandler);
14329
+ }
14330
+ },
14331
+ onConnect: (connectHandler) => {
14332
+ if (ws) {
14333
+ ws.onConnect(connectHandler);
14334
+ }
14335
+ },
14336
+ websocketUrl
14337
+ };
14338
+ const result = await handler(input, context);
14339
+ return this.formatToolResult(result, instanceId, websocketUrl);
14205
14340
  } catch (error) {
14206
14341
  const err = error instanceof Error ? error : new Error(String(error));
14207
14342
  console.error(`[MCP] Tool "${name}" failed:`, err.message);
@@ -14214,182 +14349,96 @@ var App = class {
14214
14349
  }
14215
14350
  );
14216
14351
  }
14217
- return server;
14218
14352
  }
14219
14353
  /**
14220
- * Format a tool result into MCP format.
14354
+ * Get or create a WebSocket for an instance.
14355
+ * Used internally by registerTools when resource has websocket: true.
14356
+ */
14357
+ getOrCreateWebSocket(instanceId) {
14358
+ const existing = this.instanceWebSockets.get(instanceId);
14359
+ if (existing) {
14360
+ return existing;
14361
+ }
14362
+ const ws = this.websocketManager.createWebSocket(instanceId, {}, this.getPort());
14363
+ this.instanceWebSockets.set(instanceId, ws);
14364
+ console.log(`[MCP] WebSocket created for instance ${instanceId}`);
14365
+ return ws;
14366
+ }
14367
+ buildToolMeta(config) {
14368
+ const toolMeta = {};
14369
+ if (config.ui) {
14370
+ toolMeta.ui = {
14371
+ resourceUri: config.ui,
14372
+ visibility: config.visibility || ["model", "app"],
14373
+ ...config.displayModes && { displayModes: config.displayModes },
14374
+ ...config.defaultDisplayMode && { defaultDisplayMode: config.defaultDisplayMode }
14375
+ };
14376
+ toolMeta["openai/outputTemplate"] = config.ui;
14377
+ }
14378
+ if (config.loadingMessage) {
14379
+ toolMeta["openai/toolInvocation/invoking"] = config.loadingMessage;
14380
+ }
14381
+ if (config.completedMessage) {
14382
+ toolMeta["openai/toolInvocation/invoked"] = config.completedMessage;
14383
+ }
14384
+ return toolMeta;
14385
+ }
14386
+ buildToolDescription(config, inputSchema) {
14387
+ let description = config.description;
14388
+ if (config.ui) {
14389
+ const schemaShape = this.getSchemaShape(inputSchema);
14390
+ const hasInstanceIdInSchema = schemaShape && "instanceId" in schemaShape;
14391
+ const isInstanceIdRequired = this.isFieldRequired(inputSchema, "instanceId");
14392
+ if (isInstanceIdRequired) {
14393
+ description += " Requires instanceId from a previous tool response.";
14394
+ } else if (hasInstanceIdInSchema) {
14395
+ description += " Returns instanceId. Pass it in subsequent calls to target the same widget.";
14396
+ } else {
14397
+ description += " Returns instanceId for the new widget.";
14398
+ }
14399
+ }
14400
+ return description;
14401
+ }
14402
+ /**
14403
+ * Format tool result for MCP protocol response.
14404
+ *
14405
+ * SDK manages instanceId and websocketUrl automatically.
14221
14406
  */
14222
- formatToolResult(result) {
14407
+ formatToolResult(result, instanceId, websocketUrl) {
14223
14408
  const text = result.text || JSON.stringify(result.data || {});
14224
14409
  const structuredContent = {
14225
14410
  ...result.data,
14226
14411
  ...result.title && { title: result.title },
14227
- ...result.inlineHeight && { inlineHeight: result.inlineHeight }
14412
+ ...result.inlineHeight && { inlineHeight: result.inlineHeight },
14413
+ ...instanceId && { instanceId },
14414
+ ...websocketUrl && { websocketUrl }
14228
14415
  };
14416
+ const meta = {};
14417
+ if (instanceId) {
14418
+ meta["openai/widgetSessionId"] = instanceId;
14419
+ }
14229
14420
  return {
14230
14421
  content: [{ type: "text", text }],
14231
14422
  ...Object.keys(structuredContent).length > 0 && { structuredContent },
14423
+ ...Object.keys(meta).length > 0 && { _meta: meta },
14232
14424
  ...result.isError && { isError: true }
14233
14425
  };
14234
14426
  }
14235
- async start() {
14236
- const port = this.getPort();
14237
- const expressApp = express();
14238
- expressApp.use(express.json());
14239
- expressApp.use((req, res, next) => {
14240
- res.setHeader("Access-Control-Allow-Origin", "*");
14241
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
14242
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Mcp-Session-Id");
14243
- res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
14244
- if (req.method === "OPTIONS") {
14245
- res.status(204).end();
14246
- return;
14247
- }
14248
- next();
14249
- });
14250
- expressApp.get("/health", (_req, res) => {
14251
- res.json({
14252
- status: "ok",
14253
- server: this.config.name,
14254
- version: this.config.version,
14255
- activeSessions: this.transports.size,
14256
- channels: this.channels.size
14257
- });
14258
- });
14259
- expressApp.post("/mcp", async (req, res) => {
14260
- const transportSessionId = req.headers["mcp-session-id"];
14261
- try {
14262
- let transport;
14263
- if (transportSessionId && this.transports.has(transportSessionId)) {
14264
- transport = this.transports.get(transportSessionId);
14265
- } else if (!transportSessionId && isInitializeRequest2(req.body)) {
14266
- transport = new StreamableHTTPServerTransport({
14267
- sessionIdGenerator: () => randomUUID(),
14268
- onsessioninitialized: (newSessionId) => {
14269
- this.transports.set(newSessionId, transport);
14270
- console.log(`[MCP] Transport session created: ${newSessionId}`);
14271
- this.config.onTransportSessionCreated?.({
14272
- id: newSessionId,
14273
- transport: "streamable-http"
14274
- });
14275
- }
14276
- });
14277
- transport.onerror = (error) => {
14278
- const sid = transport.sessionId || "unknown";
14279
- console.error(`[MCP] Transport session error for ${sid}:`, error.message);
14280
- this.config.onTransportSessionError?.(
14281
- { id: sid, transport: "streamable-http" },
14282
- error
14283
- );
14284
- };
14285
- transport.onclose = () => {
14286
- const sid = transport.sessionId;
14287
- if (sid) {
14288
- console.log(`[MCP] Transport session closed: ${sid}. Client should re-initialize to continue.`);
14289
- this.transports.delete(sid);
14290
- this.config.onTransportSessionClosed?.({
14291
- id: sid,
14292
- transport: "streamable-http"
14293
- });
14294
- }
14295
- };
14296
- const server = this.createMcpServer();
14297
- await server.connect(transport);
14298
- await transport.handleRequest(req, res, req.body);
14299
- return;
14300
- } else {
14301
- res.status(400).json({
14302
- jsonrpc: "2.0",
14303
- error: { code: -32e3, message: "Bad Request: No valid transport session ID" },
14304
- id: null
14305
- });
14306
- return;
14307
- }
14308
- await transport.handleRequest(req, res, req.body);
14309
- } catch (error) {
14310
- console.error("Error:", error);
14311
- if (!res.headersSent) {
14312
- res.status(500).json({
14313
- jsonrpc: "2.0",
14314
- error: { code: -32603, message: "Internal server error" },
14315
- id: null
14316
- });
14317
- }
14318
- }
14319
- });
14320
- expressApp.get("/mcp", async (req, res) => {
14321
- const transportSessionId = req.headers["mcp-session-id"];
14322
- if (!transportSessionId || !this.transports.has(transportSessionId)) {
14323
- res.status(400).send("Invalid or missing transport session ID");
14324
- return;
14325
- }
14326
- const transport = this.transports.get(transportSessionId);
14327
- await transport.handleRequest(req, res);
14328
- });
14329
- expressApp.delete("/mcp", async (req, res) => {
14330
- const transportSessionId = req.headers["mcp-session-id"];
14331
- if (!transportSessionId || !this.transports.has(transportSessionId)) {
14332
- res.status(400).send("Invalid or missing transport session ID");
14333
- return;
14334
- }
14335
- const transport = this.transports.get(transportSessionId);
14336
- await transport.handleRequest(req, res);
14337
- });
14338
- this.httpServer = expressApp.listen(port, () => {
14339
- console.log(`MCP server ready on port ${port}`);
14340
- });
14341
- this.channelManager.attach(this.httpServer);
14342
- console.log(`WebSocket channels available at ws://localhost:${port}/channels`);
14343
- if (!this.shutdownRegistered) {
14344
- this.shutdownRegistered = true;
14345
- const handleShutdown = async (signal) => {
14346
- if (this.isShuttingDown) return;
14347
- this.isShuttingDown = true;
14348
- console.log(`[MCP] ${signal} received, shutting down gracefully...`);
14349
- await this.stop();
14350
- process.exit(0);
14351
- };
14352
- process.on("SIGINT", () => handleShutdown("SIGINT"));
14353
- process.on("SIGTERM", () => handleShutdown("SIGTERM"));
14354
- }
14355
- }
14356
- /**
14357
- * Stop the MCP server gracefully.
14358
- *
14359
- * Calls the onShutdown callback, closes all sessions and channels,
14360
- * then waits for active transports to close (with timeout).
14361
- */
14362
- async stop() {
14363
- const gracefulTimeout = this.config.gracefulShutdownTimeout ?? 5e3;
14364
- const keepAliveTimeout = this.config.keepAliveTimeout ?? 5e3;
14365
- try {
14366
- await this.config.onShutdown?.();
14367
- } catch (error) {
14368
- console.error("[MCP] Error in onShutdown callback:", error);
14369
- }
14370
- this.appSessionManager.closeAll();
14371
- this.channelManager.closeAll();
14372
- const transportClosePromises = [...this.transports.values()].map(
14373
- (transport) => transport.close().catch(() => {
14374
- })
14375
- );
14376
- await Promise.race([
14377
- Promise.all(transportClosePromises),
14378
- new Promise((resolve2) => setTimeout(resolve2, gracefulTimeout))
14379
- ]);
14380
- this.transports.clear();
14381
- if (this.httpServer) {
14382
- this.httpServer.keepAliveTimeout = keepAliveTimeout;
14383
- await new Promise((resolve2) => {
14384
- const forceClose = setTimeout(resolve2, gracefulTimeout);
14385
- this.httpServer?.close(() => {
14386
- clearTimeout(forceClose);
14387
- resolve2();
14388
- });
14389
- });
14390
- this.httpServer = null;
14391
- }
14392
- console.log("[MCP] Shutdown complete");
14427
+ // ==========================================================================
14428
+ // Private: Shutdown
14429
+ // ==========================================================================
14430
+ registerShutdownHandlers() {
14431
+ if (this.shutdownRegistered) return;
14432
+ this.shutdownRegistered = true;
14433
+ const handleShutdown = async (signal) => {
14434
+ if (this.isShuttingDown) return;
14435
+ this.isShuttingDown = true;
14436
+ console.log(`[MCP] ${signal} received, shutting down gracefully...`);
14437
+ await this.stop();
14438
+ process.exit(0);
14439
+ };
14440
+ process.on("SIGINT", () => handleShutdown("SIGINT"));
14441
+ process.on("SIGTERM", () => handleShutdown("SIGTERM"));
14393
14442
  }
14394
14443
  };
14395
14444
  function isSDKPath(filename) {
@@ -14477,10 +14526,7 @@ function wrapServer(server) {
14477
14526
  }
14478
14527
  export {
14479
14528
  App,
14480
- AppSessionManager,
14481
- ChannelDefinition,
14482
14529
  MIME_TYPES,
14483
- ServerAppSession,
14484
14530
  createApp,
14485
14531
  htmlLoader,
14486
14532
  loadHtml,