@creature-ai/sdk 0.1.2 → 0.1.4

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