@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.
- package/dist/core/index.d.ts +299 -72
- package/dist/core/index.js +235 -267
- package/dist/core/index.js.map +1 -1
- package/dist/react/index.d.ts +47 -51
- package/dist/react/index.js +249 -311
- package/dist/react/index.js.map +1 -1
- package/dist/server/index.d.ts +187 -218
- package/dist/server/index.js +583 -524
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/types-JBEuUzEi.d.ts +0 -186
package/dist/server/index.js
CHANGED
|
@@ -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 = [
|
|
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,
|
|
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 :
|
|
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(
|
|
156
|
+
return new _Code(safeStringify(x));
|
|
157
157
|
}
|
|
158
158
|
exports.stringify = stringify;
|
|
159
|
-
function
|
|
159
|
+
function safeStringify(x) {
|
|
160
160
|
return JSON.stringify(x).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
161
161
|
}
|
|
162
|
-
exports.safeStringify =
|
|
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/
|
|
13566
|
+
// src/server/websocket.ts
|
|
13567
13567
|
import { WebSocketServer, WebSocket } from "ws";
|
|
13568
|
-
var
|
|
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(`[
|
|
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, "
|
|
13613
|
+
client.close(1e3, "WebSocket closed");
|
|
13614
13614
|
}
|
|
13615
13615
|
this.clients.clear();
|
|
13616
13616
|
}
|
|
13617
13617
|
};
|
|
13618
|
-
var
|
|
13618
|
+
var WebSocketManager = class {
|
|
13619
13619
|
wss = null;
|
|
13620
|
-
|
|
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
|
-
*
|
|
13630
|
+
* Lazily attach the WebSocket server when first needed.
|
|
13623
13631
|
*/
|
|
13624
|
-
|
|
13632
|
+
ensureAttached() {
|
|
13633
|
+
if (this.isAttached || !this.httpServer) return;
|
|
13625
13634
|
this.wss = new WebSocketServer({ noServer: true });
|
|
13626
|
-
|
|
13635
|
+
this.httpServer.on("upgrade", (request, socket, head) => {
|
|
13627
13636
|
const url = request.url || "";
|
|
13628
|
-
if (!url.startsWith("/
|
|
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(/^\/
|
|
13646
|
+
const match = req.url?.match(/^\/ws\/(.+)$/);
|
|
13638
13647
|
if (!match) {
|
|
13639
|
-
ws.close(4e3, "Invalid
|
|
13648
|
+
ws.close(4e3, "Invalid WebSocket URL");
|
|
13640
13649
|
return;
|
|
13641
13650
|
}
|
|
13642
|
-
const [,
|
|
13643
|
-
const
|
|
13644
|
-
|
|
13645
|
-
|
|
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
|
-
|
|
13657
|
+
connection.addClient(ws);
|
|
13650
13658
|
ws.on("message", (data) => {
|
|
13651
13659
|
try {
|
|
13652
13660
|
const message = JSON.parse(data.toString());
|
|
13653
|
-
|
|
13661
|
+
connection.handleMessage(message);
|
|
13654
13662
|
} catch (e) {
|
|
13655
|
-
console.error(`[
|
|
13663
|
+
console.error(`[WebSocket] Failed to parse message:`, e);
|
|
13656
13664
|
}
|
|
13657
13665
|
});
|
|
13658
13666
|
ws.on("close", () => {
|
|
13659
|
-
|
|
13667
|
+
connection.removeClient(ws);
|
|
13660
13668
|
});
|
|
13661
13669
|
ws.on("error", (error) => {
|
|
13662
|
-
console.error(`[
|
|
13663
|
-
|
|
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
|
|
13678
|
+
* Create a WebSocket connection for an instance.
|
|
13679
|
+
* Lazily initializes the WebSocket server on first call.
|
|
13669
13680
|
*
|
|
13670
|
-
* @param
|
|
13671
|
-
* @param
|
|
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
|
|
13684
|
+
* @returns A WebSocketConnection for bidirectional communication
|
|
13675
13685
|
*/
|
|
13676
|
-
|
|
13677
|
-
|
|
13678
|
-
const existing = this.
|
|
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
|
|
13683
|
-
this.
|
|
13692
|
+
const internal = new WebSocketConnectionInternal(instanceId, config);
|
|
13693
|
+
this.connections.set(instanceId, internal);
|
|
13684
13694
|
return {
|
|
13685
|
-
|
|
13686
|
-
|
|
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.
|
|
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
|
|
13710
|
+
* Check if a WebSocket exists for an instance.
|
|
13701
13711
|
*/
|
|
13702
|
-
|
|
13703
|
-
return this.
|
|
13712
|
+
hasWebSocket(instanceId) {
|
|
13713
|
+
return this.connections.has(instanceId);
|
|
13704
13714
|
}
|
|
13705
13715
|
/**
|
|
13706
|
-
*
|
|
13716
|
+
* Close a specific instance's WebSocket.
|
|
13707
13717
|
*/
|
|
13708
|
-
|
|
13709
|
-
|
|
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
|
|
13728
|
+
* Close all WebSocket connections and shut down the server.
|
|
13713
13729
|
*/
|
|
13714
13730
|
closeAll() {
|
|
13715
|
-
for (const
|
|
13716
|
-
|
|
13731
|
+
for (const connection of this.connections.values()) {
|
|
13732
|
+
connection.closeAll();
|
|
13717
13733
|
}
|
|
13718
|
-
this.
|
|
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
|
-
|
|
13920
|
-
|
|
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
|
-
|
|
13946
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
14046
|
-
|
|
14047
|
-
|
|
14048
|
-
|
|
14049
|
-
|
|
14050
|
-
|
|
14051
|
-
|
|
14052
|
-
|
|
14053
|
-
|
|
14054
|
-
|
|
14055
|
-
|
|
14056
|
-
|
|
14057
|
-
|
|
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
|
-
|
|
14060
|
-
|
|
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
|
-
|
|
14064
|
-
|
|
14065
|
-
|
|
14066
|
-
|
|
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
|
|
14070
|
-
|
|
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
|
-
|
|
14077
|
-
|
|
14078
|
-
|
|
14079
|
-
|
|
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
|
-
|
|
14082
|
-
|
|
14083
|
-
|
|
14084
|
-
|
|
14085
|
-
|
|
14086
|
-
|
|
14087
|
-
|
|
14088
|
-
|
|
14089
|
-
)
|
|
14090
|
-
|
|
14091
|
-
|
|
14092
|
-
|
|
14093
|
-
|
|
14094
|
-
|
|
14095
|
-
|
|
14096
|
-
|
|
14097
|
-
|
|
14098
|
-
|
|
14099
|
-
|
|
14100
|
-
return
|
|
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
|
|
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
|
-
|
|
14204
|
-
|
|
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
|
-
*
|
|
14367
|
+
* Get or create a WebSocket for an instance.
|
|
14368
|
+
* Used internally by registerTools when resource has websocket: true.
|
|
14221
14369
|
*/
|
|
14222
|
-
|
|
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
|
-
|
|
14236
|
-
|
|
14237
|
-
|
|
14238
|
-
|
|
14239
|
-
|
|
14240
|
-
|
|
14241
|
-
|
|
14242
|
-
|
|
14243
|
-
|
|
14244
|
-
|
|
14245
|
-
|
|
14246
|
-
|
|
14247
|
-
|
|
14248
|
-
|
|
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,
|