@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/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
- logger.error(
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
- logger.error(
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