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