@mcp-fe/mcp-worker 0.0.17 → 0.1.0
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/README.md +184 -349
- package/docs/api.md +418 -0
- package/docs/architecture.md +195 -0
- package/docs/guide.md +454 -0
- package/docs/index.md +109 -0
- package/docs/initialization.md +188 -0
- package/docs/worker-details.md +435 -0
- package/index.js +365 -5
- package/mcp-service-worker.js +505 -204
- package/mcp-shared-worker.js +487 -173
- package/package.json +1 -1
- package/src/index.d.ts +2 -1
- package/src/index.d.ts.map +1 -1
- package/src/lib/built-in-tools.d.ts +9 -0
- package/src/lib/built-in-tools.d.ts.map +1 -0
- package/src/lib/mcp-controller.d.ts +18 -0
- package/src/lib/mcp-controller.d.ts.map +1 -1
- package/src/lib/mcp-server.d.ts +11 -0
- package/src/lib/mcp-server.d.ts.map +1 -1
- package/src/lib/tool-registry.d.ts +28 -0
- package/src/lib/tool-registry.d.ts.map +1 -0
- package/src/lib/worker-client.d.ts +123 -0
- package/src/lib/worker-client.d.ts.map +1 -1
package/index.js
CHANGED
|
@@ -42,6 +42,17 @@ var WorkerClient = class {
|
|
|
42
42
|
connectionStatusCallbacks = /* @__PURE__ */ new Set();
|
|
43
43
|
// Mutex/promise to prevent concurrent init runs
|
|
44
44
|
initPromise = null;
|
|
45
|
+
// Initialization state
|
|
46
|
+
isInitialized = false;
|
|
47
|
+
initResolvers = [];
|
|
48
|
+
// Queue for operations that need to wait for initialization
|
|
49
|
+
pendingRegistrations = [];
|
|
50
|
+
// Map to store tool handlers in main thread
|
|
51
|
+
toolHandlers = /* @__PURE__ */ new Map();
|
|
52
|
+
// Tool registry for tracking registrations and reference counting
|
|
53
|
+
toolRegistry = /* @__PURE__ */ new Map();
|
|
54
|
+
// Subscribers for tool changes (for React hooks reactivity)
|
|
55
|
+
toolChangeListeners = /* @__PURE__ */ new Map();
|
|
45
56
|
// Initialize and choose worker implementation (prefer SharedWorker)
|
|
46
57
|
// Accept either a ServiceWorkerRegistration OR WorkerInitOptions to configure URLs
|
|
47
58
|
async init(registrationOrOptions) {
|
|
@@ -98,18 +109,72 @@ var WorkerClient = class {
|
|
|
98
109
|
return;
|
|
99
110
|
}
|
|
100
111
|
const sharedOk = await this.initSharedWorker();
|
|
101
|
-
if (sharedOk)
|
|
112
|
+
if (sharedOk) {
|
|
113
|
+
this.markAsInitialized();
|
|
102
114
|
return;
|
|
115
|
+
}
|
|
103
116
|
await this.initServiceWorkerFallback();
|
|
117
|
+
this.markAsInitialized();
|
|
104
118
|
} finally {
|
|
105
119
|
this.initPromise = null;
|
|
106
120
|
}
|
|
107
121
|
})();
|
|
108
122
|
return this.initPromise;
|
|
109
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Mark worker as initialized and process pending registrations
|
|
126
|
+
* @private
|
|
127
|
+
*/
|
|
128
|
+
markAsInitialized() {
|
|
129
|
+
this.isInitialized = true;
|
|
130
|
+
logger.log(
|
|
131
|
+
"[WorkerClient] Worker initialized, processing pending operations"
|
|
132
|
+
);
|
|
133
|
+
this.initResolvers.forEach((resolve) => resolve());
|
|
134
|
+
this.initResolvers = [];
|
|
135
|
+
const pending = [...this.pendingRegistrations];
|
|
136
|
+
this.pendingRegistrations = [];
|
|
137
|
+
pending.forEach(
|
|
138
|
+
async ({ name, description, inputSchema, handler, resolve, reject }) => {
|
|
139
|
+
try {
|
|
140
|
+
await this.registerToolInternal(
|
|
141
|
+
name,
|
|
142
|
+
description,
|
|
143
|
+
inputSchema,
|
|
144
|
+
handler
|
|
145
|
+
);
|
|
146
|
+
resolve();
|
|
147
|
+
} catch (error) {
|
|
148
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Wait for worker initialization
|
|
155
|
+
* @returns Promise that resolves when worker is initialized
|
|
156
|
+
*/
|
|
157
|
+
async waitForInit() {
|
|
158
|
+
if (this.isInitialized) {
|
|
159
|
+
return Promise.resolve();
|
|
160
|
+
}
|
|
161
|
+
if (this.initPromise) {
|
|
162
|
+
await this.initPromise;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
return new Promise((resolve) => {
|
|
166
|
+
this.initResolvers.push(resolve);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Check if worker is initialized
|
|
171
|
+
*/
|
|
172
|
+
get initialized() {
|
|
173
|
+
return this.isInitialized;
|
|
174
|
+
}
|
|
110
175
|
async initSharedWorker() {
|
|
111
176
|
if (typeof SharedWorker === "undefined") {
|
|
112
|
-
return false;
|
|
177
|
+
return Promise.resolve(false);
|
|
113
178
|
}
|
|
114
179
|
try {
|
|
115
180
|
this.sharedWorker = new SharedWorker(this.sharedWorkerUrl, {
|
|
@@ -140,7 +205,7 @@ var WorkerClient = class {
|
|
|
140
205
|
resolved = true;
|
|
141
206
|
this.workerType = "shared";
|
|
142
207
|
p.onmessage = null;
|
|
143
|
-
resolve();
|
|
208
|
+
resolve(true);
|
|
144
209
|
}
|
|
145
210
|
} catch {
|
|
146
211
|
}
|
|
@@ -174,6 +239,15 @@ var WorkerClient = class {
|
|
|
174
239
|
} catch {
|
|
175
240
|
}
|
|
176
241
|
});
|
|
242
|
+
} else if (data && data.type === "CALL_TOOL") {
|
|
243
|
+
this.handleToolCall(data.toolName, data.args, data.callId).catch(
|
|
244
|
+
(error) => {
|
|
245
|
+
logger.error(
|
|
246
|
+
"[WorkerClient] Failed to handle tool call:",
|
|
247
|
+
error
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
);
|
|
177
251
|
}
|
|
178
252
|
} catch {
|
|
179
253
|
}
|
|
@@ -205,6 +279,33 @@ var WorkerClient = class {
|
|
|
205
279
|
this.serviceWorkerUrl
|
|
206
280
|
);
|
|
207
281
|
this.workerType = "service";
|
|
282
|
+
if ("serviceWorker" in navigator) {
|
|
283
|
+
navigator.serviceWorker.addEventListener(
|
|
284
|
+
"message",
|
|
285
|
+
(ev) => {
|
|
286
|
+
try {
|
|
287
|
+
const data = ev.data;
|
|
288
|
+
if (data && data.type === "CALL_TOOL") {
|
|
289
|
+
this.handleToolCall(
|
|
290
|
+
data.toolName,
|
|
291
|
+
data.args,
|
|
292
|
+
data.callId
|
|
293
|
+
).catch((error) => {
|
|
294
|
+
logger.error(
|
|
295
|
+
"[WorkerClient] Failed to handle tool call:",
|
|
296
|
+
error
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
} catch (error) {
|
|
301
|
+
logger.error(
|
|
302
|
+
"[WorkerClient] Error processing ServiceWorker message:",
|
|
303
|
+
error
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
);
|
|
308
|
+
}
|
|
208
309
|
logger.info("[WorkerClient] Using MCP ServiceWorker (fallback)");
|
|
209
310
|
try {
|
|
210
311
|
const initMsg = {
|
|
@@ -430,7 +531,7 @@ var WorkerClient = class {
|
|
|
430
531
|
token
|
|
431
532
|
});
|
|
432
533
|
} catch (e) {
|
|
433
|
-
|
|
534
|
+
console.error(
|
|
434
535
|
"[WorkerClient] Failed to send auth token to ServiceWorker:",
|
|
435
536
|
e
|
|
436
537
|
);
|
|
@@ -442,7 +543,7 @@ var WorkerClient = class {
|
|
|
442
543
|
token
|
|
443
544
|
});
|
|
444
545
|
} catch (e) {
|
|
445
|
-
|
|
546
|
+
console.error(
|
|
446
547
|
"[WorkerClient] Failed to send auth token to ServiceWorker.controller:",
|
|
447
548
|
e
|
|
448
549
|
);
|
|
@@ -490,6 +591,264 @@ var WorkerClient = class {
|
|
|
490
591
|
} else {
|
|
491
592
|
}
|
|
492
593
|
}
|
|
594
|
+
/**
|
|
595
|
+
* Register a custom MCP tool dynamically
|
|
596
|
+
*
|
|
597
|
+
* The handler function runs in the MAIN THREAD (browser context), not in the worker.
|
|
598
|
+
* This means you have full access to:
|
|
599
|
+
* - React context, hooks, Redux store
|
|
600
|
+
* - DOM API, window, localStorage
|
|
601
|
+
* - All your imports and dependencies
|
|
602
|
+
* - Closures and external variables
|
|
603
|
+
*
|
|
604
|
+
* The worker acts as a proxy - it receives MCP tool calls and forwards them
|
|
605
|
+
* to your handler via MessageChannel.
|
|
606
|
+
*
|
|
607
|
+
* @param name - Tool name
|
|
608
|
+
* @param description - Tool description
|
|
609
|
+
* @param inputSchema - JSON Schema for tool inputs
|
|
610
|
+
* @param handler - Async function that handles the tool execution (runs in main thread)
|
|
611
|
+
* @returns Promise that resolves when tool is registered
|
|
612
|
+
*
|
|
613
|
+
* @example With full access to imports and context:
|
|
614
|
+
* ```typescript
|
|
615
|
+
* import { z } from 'zod';
|
|
616
|
+
* import { useMyStore } from './store';
|
|
617
|
+
*
|
|
618
|
+
* const store = useMyStore();
|
|
619
|
+
*
|
|
620
|
+
* await client.registerTool(
|
|
621
|
+
* 'validate_user',
|
|
622
|
+
* 'Validate user data',
|
|
623
|
+
* { type: 'object', properties: { username: { type: 'string' } } },
|
|
624
|
+
* async (args: any) => {
|
|
625
|
+
* // Full access to everything!
|
|
626
|
+
* const schema = z.object({ username: z.string().min(3) });
|
|
627
|
+
* const validated = schema.parse(args);
|
|
628
|
+
*
|
|
629
|
+
* // Can access store, context, etc.
|
|
630
|
+
* const currentUser = store.getState().user;
|
|
631
|
+
*
|
|
632
|
+
* return {
|
|
633
|
+
* content: [{
|
|
634
|
+
* type: 'text',
|
|
635
|
+
* text: JSON.stringify({ validated, currentUser })
|
|
636
|
+
* }]
|
|
637
|
+
* };
|
|
638
|
+
* }
|
|
639
|
+
* );
|
|
640
|
+
* ```
|
|
641
|
+
*/
|
|
642
|
+
async registerTool(name, description, inputSchema, handler) {
|
|
643
|
+
if (!this.isInitialized) {
|
|
644
|
+
logger.log(
|
|
645
|
+
`[WorkerClient] Queueing tool registration '${name}' (worker not initialized yet)`
|
|
646
|
+
);
|
|
647
|
+
return new Promise((resolve, reject) => {
|
|
648
|
+
this.pendingRegistrations.push({
|
|
649
|
+
name,
|
|
650
|
+
description,
|
|
651
|
+
inputSchema,
|
|
652
|
+
handler,
|
|
653
|
+
resolve,
|
|
654
|
+
reject
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
return this.registerToolInternal(name, description, inputSchema, handler);
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Internal method to register tool (assumes worker is initialized)
|
|
662
|
+
* @private
|
|
663
|
+
*/
|
|
664
|
+
async registerToolInternal(name, description, inputSchema, handler) {
|
|
665
|
+
const existing = this.toolRegistry.get(name);
|
|
666
|
+
if (existing) {
|
|
667
|
+
existing.refCount++;
|
|
668
|
+
logger.log(
|
|
669
|
+
`[WorkerClient] Incremented ref count for '${name}': ${existing.refCount}`
|
|
670
|
+
);
|
|
671
|
+
this.toolHandlers.set(name, handler);
|
|
672
|
+
this.notifyToolChange(name);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
this.toolHandlers.set(name, handler);
|
|
676
|
+
await this.request("REGISTER_TOOL", {
|
|
677
|
+
name,
|
|
678
|
+
description,
|
|
679
|
+
inputSchema,
|
|
680
|
+
handlerType: "proxy"
|
|
681
|
+
// Tell worker this is a proxy handler
|
|
682
|
+
});
|
|
683
|
+
this.toolRegistry.set(name, {
|
|
684
|
+
refCount: 1,
|
|
685
|
+
description,
|
|
686
|
+
inputSchema,
|
|
687
|
+
isRegistered: true
|
|
688
|
+
});
|
|
689
|
+
logger.log(
|
|
690
|
+
`[WorkerClient] Registered tool '${name}' with main-thread handler`
|
|
691
|
+
);
|
|
692
|
+
this.notifyToolChange(name);
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Unregister a previously registered MCP tool
|
|
696
|
+
* @param name - Tool name to unregister
|
|
697
|
+
* @returns Promise that resolves to true if tool was found and removed
|
|
698
|
+
*/
|
|
699
|
+
async unregisterTool(name) {
|
|
700
|
+
const existing = this.toolRegistry.get(name);
|
|
701
|
+
if (!existing) {
|
|
702
|
+
logger.warn(`[WorkerClient] Cannot unregister '${name}': not found`);
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
existing.refCount--;
|
|
706
|
+
logger.log(
|
|
707
|
+
`[WorkerClient] Decremented ref count for '${name}': ${existing.refCount}`
|
|
708
|
+
);
|
|
709
|
+
if (existing.refCount <= 0) {
|
|
710
|
+
this.toolHandlers.delete(name);
|
|
711
|
+
const result = await this.request(
|
|
712
|
+
"UNREGISTER_TOOL",
|
|
713
|
+
{ name }
|
|
714
|
+
);
|
|
715
|
+
this.toolRegistry.delete(name);
|
|
716
|
+
logger.log(`[WorkerClient] Unregistered tool '${name}'`);
|
|
717
|
+
this.notifyToolChange(name);
|
|
718
|
+
return result?.success ?? false;
|
|
719
|
+
}
|
|
720
|
+
this.notifyToolChange(name);
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Subscribe to tool changes for a specific tool
|
|
725
|
+
* Returns unsubscribe function
|
|
726
|
+
*/
|
|
727
|
+
onToolChange(toolName, callback) {
|
|
728
|
+
if (!this.toolChangeListeners.has(toolName)) {
|
|
729
|
+
this.toolChangeListeners.set(toolName, /* @__PURE__ */ new Set());
|
|
730
|
+
}
|
|
731
|
+
const listeners = this.toolChangeListeners.get(toolName);
|
|
732
|
+
listeners.add(callback);
|
|
733
|
+
const current = this.toolRegistry.get(toolName);
|
|
734
|
+
if (current) {
|
|
735
|
+
callback({
|
|
736
|
+
refCount: current.refCount,
|
|
737
|
+
isRegistered: current.isRegistered
|
|
738
|
+
});
|
|
739
|
+
} else {
|
|
740
|
+
callback(null);
|
|
741
|
+
}
|
|
742
|
+
return () => {
|
|
743
|
+
listeners.delete(callback);
|
|
744
|
+
if (listeners.size === 0) {
|
|
745
|
+
this.toolChangeListeners.delete(toolName);
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Notify all listeners about tool changes
|
|
751
|
+
* @private
|
|
752
|
+
*/
|
|
753
|
+
notifyToolChange(toolName) {
|
|
754
|
+
const listeners = this.toolChangeListeners.get(toolName);
|
|
755
|
+
if (!listeners || listeners.size === 0)
|
|
756
|
+
return;
|
|
757
|
+
const info = this.toolRegistry.get(toolName);
|
|
758
|
+
const payload = info ? {
|
|
759
|
+
refCount: info.refCount,
|
|
760
|
+
isRegistered: info.isRegistered
|
|
761
|
+
} : null;
|
|
762
|
+
listeners.forEach((callback) => {
|
|
763
|
+
try {
|
|
764
|
+
callback(payload);
|
|
765
|
+
} catch (error) {
|
|
766
|
+
logger.error("[WorkerClient] Error in tool change listener:", error);
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Get tool info from registry
|
|
772
|
+
*/
|
|
773
|
+
getToolInfo(toolName) {
|
|
774
|
+
const info = this.toolRegistry.get(toolName);
|
|
775
|
+
if (!info)
|
|
776
|
+
return null;
|
|
777
|
+
return {
|
|
778
|
+
refCount: info.refCount,
|
|
779
|
+
isRegistered: info.isRegistered
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Get all registered tool names
|
|
784
|
+
*/
|
|
785
|
+
getRegisteredTools() {
|
|
786
|
+
return Array.from(this.toolRegistry.keys()).filter(
|
|
787
|
+
(name) => this.toolRegistry.get(name)?.isRegistered
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Check if a tool is registered
|
|
792
|
+
*/
|
|
793
|
+
isToolRegistered(toolName) {
|
|
794
|
+
return this.toolRegistry.get(toolName)?.isRegistered ?? false;
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Handle tool call from worker - execute handler in main thread and return result
|
|
798
|
+
* @private
|
|
799
|
+
*/
|
|
800
|
+
async handleToolCall(toolName, args, callId) {
|
|
801
|
+
logger.log(`[WorkerClient] Handling tool call: ${toolName}`, {
|
|
802
|
+
callId,
|
|
803
|
+
args
|
|
804
|
+
});
|
|
805
|
+
try {
|
|
806
|
+
const handler = this.toolHandlers.get(toolName);
|
|
807
|
+
if (!handler) {
|
|
808
|
+
throw new Error(`Tool handler not found: ${toolName}`);
|
|
809
|
+
}
|
|
810
|
+
const result = await handler(args);
|
|
811
|
+
this.sendToolCallResult(callId, { success: true, result });
|
|
812
|
+
} catch (error) {
|
|
813
|
+
logger.error(`[WorkerClient] Tool call failed: ${toolName}`, error);
|
|
814
|
+
this.sendToolCallResult(callId, {
|
|
815
|
+
success: false,
|
|
816
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Send tool call result back to worker
|
|
822
|
+
* @private
|
|
823
|
+
*/
|
|
824
|
+
sendToolCallResult(callId, result) {
|
|
825
|
+
const message = {
|
|
826
|
+
type: "TOOL_CALL_RESULT",
|
|
827
|
+
callId,
|
|
828
|
+
success: result.success,
|
|
829
|
+
result: result.result,
|
|
830
|
+
error: result.error
|
|
831
|
+
};
|
|
832
|
+
if (this.workerType === "shared" && this.sharedWorkerPort) {
|
|
833
|
+
try {
|
|
834
|
+
this.sharedWorkerPort.postMessage(message);
|
|
835
|
+
} catch (error) {
|
|
836
|
+
logger.error(
|
|
837
|
+
"[WorkerClient] Failed to send result to SharedWorker:",
|
|
838
|
+
error
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
} else if (this.workerType === "service" && this.serviceWorkerRegistration?.active) {
|
|
842
|
+
try {
|
|
843
|
+
this.serviceWorkerRegistration.active.postMessage(message);
|
|
844
|
+
} catch (error) {
|
|
845
|
+
logger.error(
|
|
846
|
+
"[WorkerClient] Failed to send result to ServiceWorker:",
|
|
847
|
+
error
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
493
852
|
};
|
|
494
853
|
|
|
495
854
|
// libs/mcp-worker/src/lib/database.ts
|
|
@@ -546,6 +905,7 @@ async function queryEvents(filters) {
|
|
|
546
905
|
// libs/mcp-worker/src/index.ts
|
|
547
906
|
var workerClient = new WorkerClient();
|
|
548
907
|
export {
|
|
908
|
+
WorkerClient,
|
|
549
909
|
logger,
|
|
550
910
|
queryEvents,
|
|
551
911
|
workerClient
|