@r0hitsharma/webmcp 0.12.0-rohit-fork-ci.1

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.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * StrictMode-safe tool registration against the WebMCP polyfill.
3
+ *
4
+ * Why this exists: the pinned `@mcp-b` polyfill bridges `document.modelContext`
5
+ * (a Map with AbortSignal-based unregister) to an underlying McpServer whose
6
+ * `_registeredTools` is synced via `queueMicrotask`. React StrictMode mounts
7
+ * effects mount -> unmount -> mount synchronously, which races that microtask
8
+ * sync and makes the McpServer throw `Tool <name> is already registered`,
9
+ * crashing the app in dev. (This is the kind of @mcp-b churn the wrapper is
10
+ * meant to absorb; see decision 8 / the plan's section 8.)
11
+ *
12
+ * The fix: register each tool name exactly once, refcounted. A transient
13
+ * unmount/remount in the same tick re-acquires the existing registration
14
+ * instead of abort+re-add, so the polyfill never sees a duplicate. The real
15
+ * unregister (signal abort) only fires once the last holder is gone, deferred
16
+ * to a microtask so a StrictMode remount can cancel it.
17
+ */
18
+ import type { ToolSpec } from './types.js';
19
+ /**
20
+ * Acquire a registration for `spec`. Returns a release function to call on
21
+ * unmount. Idempotent per tool name: concurrent holders share one underlying
22
+ * polyfill registration.
23
+ */
24
+ export declare function acquireToolRegistration(spec: ToolSpec): () => void;
@@ -0,0 +1,92 @@
1
+ const REGISTRY = new Map();
2
+ /** Resolve the polyfilled model context, preferring the current draft location. */
3
+ function getModelContext() {
4
+ if (typeof window === 'undefined')
5
+ return undefined;
6
+ const doc = window.document;
7
+ const nav = window.navigator;
8
+ return doc?.modelContext ?? nav?.modelContext;
9
+ }
10
+ function stringify(value) {
11
+ return typeof value === 'string' ? value : JSON.stringify(value, null, 2);
12
+ }
13
+ /**
14
+ * Acquire a registration for `spec`. Returns a release function to call on
15
+ * unmount. Idempotent per tool name: concurrent holders share one underlying
16
+ * polyfill registration.
17
+ */
18
+ export function acquireToolRegistration(spec) {
19
+ const existing = REGISTRY.get(spec.name);
20
+ if (existing) {
21
+ existing.count += 1;
22
+ existing.spec = spec; // keep the freshest handler/description
23
+ return makeRelease(spec.name);
24
+ }
25
+ const controller = new AbortController();
26
+ const entry = { count: 1, controller, spec };
27
+ REGISTRY.set(spec.name, entry);
28
+ const modelContext = getModelContext();
29
+ if (!modelContext) {
30
+ // No polyfill (SSR / unsupported browser): keep the refcount bookkeeping so
31
+ // listTools()-style local state still balances, but skip the global call.
32
+ return makeRelease(spec.name);
33
+ }
34
+ const execute = async (args) => {
35
+ try {
36
+ // Read the latest spec so deps-driven handler updates take effect without
37
+ // re-registering against the polyfill.
38
+ const current = REGISTRY.get(spec.name)?.spec ?? spec;
39
+ const result = await current.handler(args);
40
+ return { content: [{ type: 'text', text: stringify(result) }] };
41
+ }
42
+ catch (error) {
43
+ const message = error instanceof Error ? error.message : String(error);
44
+ return {
45
+ content: [{ type: 'text', text: `Error: ${message}` }],
46
+ isError: true,
47
+ };
48
+ }
49
+ };
50
+ try {
51
+ modelContext.registerTool({
52
+ name: spec.name,
53
+ description: spec.description,
54
+ inputSchema: spec.schema,
55
+ ...(spec.mutation ? { annotations: { destructiveHint: true } } : {}),
56
+ execute,
57
+ }, { signal: controller.signal });
58
+ }
59
+ catch (error) {
60
+ // Defensive: if the polyfill still reports a duplicate (e.g. a stale
61
+ // registration from a prior race), treat it as already-present rather than
62
+ // crashing the app. The existing registration stays usable.
63
+ if (!/already registered/i.test(String(error))) {
64
+ REGISTRY.delete(spec.name);
65
+ throw error;
66
+ }
67
+ }
68
+ return makeRelease(spec.name);
69
+ }
70
+ function makeRelease(name) {
71
+ let released = false;
72
+ return () => {
73
+ if (released)
74
+ return;
75
+ released = true;
76
+ const entry = REGISTRY.get(name);
77
+ if (!entry)
78
+ return;
79
+ entry.count -= 1;
80
+ if (entry.count > 0)
81
+ return;
82
+ // Defer the abort so a StrictMode remount in the same tick can re-acquire
83
+ // (bumping count back above zero) and cancel the teardown.
84
+ queueMicrotask(() => {
85
+ const current = REGISTRY.get(name);
86
+ if (current && current.count === 0) {
87
+ current.controller.abort();
88
+ REGISTRY.delete(name);
89
+ }
90
+ });
91
+ };
92
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Tool-definition contract for @r0hitsharma/webmcp.
3
+ *
4
+ * Mirrors the Python ToolSpec / define_tool idiom in the relay core
5
+ * (schema-first authoring shape).
6
+ *
7
+ * Usage:
8
+ *
9
+ * import { defineTool } from '@r0hitsharma/webmcp';
10
+ *
11
+ * const selectIdentity = defineTool({
12
+ * name: 'explorer.selectIdentity',
13
+ * description: 'Select and focus an identity node in the Explorer.',
14
+ * schema: {
15
+ * type: 'object',
16
+ * properties: { identityId: { type: 'string' } },
17
+ * required: ['identityId'],
18
+ * },
19
+ * handler: async ({ identityId }) => { ... },
20
+ * });
21
+ */
22
+ import type { ToolInputSchema } from './protocol.js';
23
+ export type ToolHandler<TArgs = Record<string, unknown>, TResult = unknown> = (args: TArgs) => Promise<TResult> | TResult;
24
+ export interface ToolSpec<TArgs = Record<string, unknown>, TResult = unknown> {
25
+ /** Namespaced tool name, e.g. "explorer.selectIdentity". */
26
+ name: string;
27
+ description: string;
28
+ schema: ToolInputSchema;
29
+ handler: ToolHandler<TArgs, TResult>;
30
+ /** True for any tool that mutates state. Triggers human-in-the-loop confirmation. */
31
+ mutation?: boolean;
32
+ /**
33
+ * Returns a one-sentence plain-English summary shown in the confirmation dialog.
34
+ * Required when mutation is true.
35
+ */
36
+ confirmationSummary?: (args: TArgs) => string;
37
+ }
38
+ /**
39
+ * Define a tool using the schema-first idiom.
40
+ *
41
+ * This is a pass-through factory; the returned value is the same object.
42
+ * It exists to provide type inference on the handler args and to make tool
43
+ * definitions self-documenting at the call site.
44
+ */
45
+ export declare function defineTool<TArgs = Record<string, unknown>, TResult = unknown>(spec: ToolSpec<TArgs, TResult>): ToolSpec<TArgs, TResult>;
46
+ export interface ViewState {
47
+ selectedIdentityId?: string;
48
+ selectedContentUuid?: string;
49
+ selectedBranch?: string;
50
+ mode?: string;
51
+ [key: string]: unknown;
52
+ }
53
+ export interface ToolRegistry {
54
+ registerTool: <TArgs = Record<string, unknown>, TResult = unknown>(spec: ToolSpec<TArgs, TResult>) => () => void;
55
+ listTools: () => ToolSpec[];
56
+ getViewState: () => ViewState;
57
+ }
58
+ /**
59
+ * A mutation awaiting approval, as the browser-side UI needs to render it.
60
+ *
61
+ * Named `PendingCallPrompt` (not `PendingCall`) to avoid colliding with
62
+ * `@r0hitsharma/mcp-connect`'s richer `PendingCallRecord`, which carries
63
+ * lifecycle status. This shape is the minimal prompt: what the dialog displays.
64
+ */
65
+ export interface PendingCallPrompt {
66
+ callId: string;
67
+ toolName: string;
68
+ summary: string;
69
+ argsPreview: Record<string, unknown>;
70
+ /** ISO-8601 timestamp when the prompt was raised (for the countdown bar). */
71
+ createdAt: string;
72
+ expiresAt: string;
73
+ }
package/dist/types.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Tool-definition contract for @r0hitsharma/webmcp.
3
+ *
4
+ * Mirrors the Python ToolSpec / define_tool idiom in the relay core
5
+ * (schema-first authoring shape).
6
+ *
7
+ * Usage:
8
+ *
9
+ * import { defineTool } from '@r0hitsharma/webmcp';
10
+ *
11
+ * const selectIdentity = defineTool({
12
+ * name: 'explorer.selectIdentity',
13
+ * description: 'Select and focus an identity node in the Explorer.',
14
+ * schema: {
15
+ * type: 'object',
16
+ * properties: { identityId: { type: 'string' } },
17
+ * required: ['identityId'],
18
+ * },
19
+ * handler: async ({ identityId }) => { ... },
20
+ * });
21
+ */
22
+ /**
23
+ * Define a tool using the schema-first idiom.
24
+ *
25
+ * This is a pass-through factory; the returned value is the same object.
26
+ * It exists to provide type inference on the handler args and to make tool
27
+ * definitions self-documenting at the call site.
28
+ */
29
+ export function defineTool(spec) {
30
+ // Enforce the cross-field invariant the type system can't express cleanly:
31
+ // a mutation must carry a confirmationSummary, otherwise the human-in-the-loop
32
+ // dialog would have nothing to show. Fail loudly at authoring time.
33
+ if (spec.mutation && !spec.confirmationSummary) {
34
+ throw new Error(`defineTool("${spec.name}"): a tool with mutation:true must provide a confirmationSummary.`);
35
+ }
36
+ return spec;
37
+ }
@@ -0,0 +1,30 @@
1
+ import type { PendingCallPrompt } from './types.js';
2
+ /** Connection state surfaced to the connect UI (mirrors the indicator states). */
3
+ export type RelaySessionStatus = 'disconnected' | 'ready' | 'connected' | 'reconnecting';
4
+ export interface UseRelaySessionOptions {
5
+ /** Base URL of the relay Worker, e.g. https://mcp-relay.example.workers.dev */
6
+ relayBaseUrl: string;
7
+ /** localStorage key for persisting the session across reloads. */
8
+ storageKey: string;
9
+ /** Human-readable title sent in the `hello` frame. */
10
+ title?: string;
11
+ /** Seconds a mutation confirmation stays open before it self-expires.
12
+ * Kept under the relay's invoke timeout so a late approval is not wasted. */
13
+ confirmationWindowSeconds?: number;
14
+ }
15
+ export interface UseRelaySessionResult {
16
+ status: RelaySessionStatus;
17
+ /** Durable token the user pastes into their harness config; null until minted. */
18
+ connectionToken: string | null;
19
+ /** Most-recent-first activity log lines, capped. */
20
+ activity: string[];
21
+ /** The mutation awaiting approval (head of the queue), or null. */
22
+ pendingConfirmation: PendingCallPrompt | null;
23
+ /** Number of mutations waiting (including the active one). */
24
+ pendingQueueLength: number;
25
+ /** Approve the active mutation: run its handler and return the result. */
26
+ approve: () => void;
27
+ /** Deny the active mutation: return a denial result to the harness. */
28
+ deny: () => void;
29
+ }
30
+ export declare function useRelaySession({ relayBaseUrl, storageKey, title, confirmationWindowSeconds, }: UseRelaySessionOptions): UseRelaySessionResult;
@@ -0,0 +1,317 @@
1
+ /**
2
+ * useRelaySession
3
+ *
4
+ * Drives a relay back-channel from the browser using the tools registered via
5
+ * `useRegisterTool`. This is the piece that lets a connected harness actually
6
+ * call the page's tools:
7
+ *
8
+ * - mints (or reuses) a session against the relay Worker and keeps the
9
+ * WebSocket open, reconnecting with exponential backoff;
10
+ * - advertises the registered tools (`tools/list`) and re-advertises when the
11
+ * registry changes;
12
+ * - on `invoke`, runs the registered tool's handler and returns a `result`;
13
+ * - gates any tool declared `mutation: true` behind a local confirmation: the
14
+ * invoke is held until the user approves (handler runs, result returned) or
15
+ * denies (a denial result is returned). The relay stays a dumb pipe — no
16
+ * confirmation frames cross the wire.
17
+ *
18
+ * Must be called inside <WebMCPProvider>.
19
+ */
20
+ import { useCallback, useEffect, useRef, useState } from 'react';
21
+ import { useToolRegistryContext } from './provider.js';
22
+ function toWireTool(spec) {
23
+ return {
24
+ name: spec.name,
25
+ description: spec.description,
26
+ input_schema: spec.schema,
27
+ ...(spec.mutation ? { mutation: true } : {}),
28
+ };
29
+ }
30
+ export function useRelaySession({ relayBaseUrl, storageKey, title = 'webmcp relay session', confirmationWindowSeconds = 25, }) {
31
+ const registry = useToolRegistryContext();
32
+ const [status, setStatus] = useState('disconnected');
33
+ const [connectionToken, setConnectionToken] = useState(null);
34
+ const [activity, setActivity] = useState([]);
35
+ const [pendingQueue, setPendingQueue] = useState([]);
36
+ const wsRef = useRef(null);
37
+ const acceptedRef = useRef(false);
38
+ // Invokes held awaiting a confirmation decision, keyed by call_id.
39
+ const heldRef = useRef(new Map());
40
+ // Always read the freshest registry accessor from the WS handler. Synced
41
+ // in an effect, not during render: refs are read-only during render.
42
+ const listToolsRef = useRef(registry.listTools);
43
+ useEffect(() => {
44
+ listToolsRef.current = registry.listTools;
45
+ });
46
+ const log = useCallback((line) => {
47
+ setActivity((prev) => [`${new Date().toLocaleTimeString()} ${line}`, ...prev].slice(0, 50));
48
+ }, []);
49
+ const send = useCallback((frame) => {
50
+ const ws = wsRef.current;
51
+ if (ws && ws.readyState === WebSocket.OPEN) {
52
+ ws.send(JSON.stringify(frame));
53
+ }
54
+ }, []);
55
+ const advertiseTools = useCallback(() => {
56
+ send({
57
+ type: 'tools/list',
58
+ tools: listToolsRef.current().map(toWireTool),
59
+ });
60
+ }, [send]);
61
+ // Run a (non-mutation, or already-approved) tool and return its result frame.
62
+ const runAndRespond = useCallback(async (callId, spec, args) => {
63
+ let result;
64
+ try {
65
+ result = await spec.handler(args);
66
+ }
67
+ catch (err) {
68
+ result = { error: err.message };
69
+ }
70
+ send({ type: 'result', call_id: callId, result });
71
+ log(`-> ${JSON.stringify(result).slice(0, 160)}`);
72
+ }, [send, log]);
73
+ const resolveHead = useCallback((decision) => {
74
+ setPendingQueue((prev) => {
75
+ const head = prev[0];
76
+ if (!head)
77
+ return prev;
78
+ const held = heldRef.current.get(head.callId);
79
+ heldRef.current.delete(head.callId);
80
+ if (held) {
81
+ if (decision === 'approved') {
82
+ log(`approved ${held.spec.name}`);
83
+ void runAndRespond(head.callId, held.spec, held.args);
84
+ }
85
+ else {
86
+ log(`denied ${held.spec.name}`);
87
+ send({
88
+ type: 'result',
89
+ call_id: head.callId,
90
+ result: { denied: true, message: 'The user denied this call.' },
91
+ });
92
+ }
93
+ }
94
+ return prev.slice(1);
95
+ });
96
+ }, [log, runAndRespond, send]);
97
+ const approve = useCallback(() => resolveHead('approved'), [resolveHead]);
98
+ const deny = useCallback(() => resolveHead('denied'), [resolveHead]);
99
+ // Connect + keep the back-channel open, reconnecting with backoff.
100
+ useEffect(() => {
101
+ let closed = false;
102
+ let attempt = 0;
103
+ let reconnectTimer;
104
+ function loadStored() {
105
+ try {
106
+ const raw = localStorage.getItem(storageKey);
107
+ if (!raw)
108
+ return null;
109
+ const s = JSON.parse(raw);
110
+ if (!s.connection_token || !s.ws_url)
111
+ return null;
112
+ if (s.expires && Date.now() > s.expires)
113
+ return null;
114
+ return s;
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ }
120
+ async function mintSession() {
121
+ const res = await fetch(`${relayBaseUrl}/api/sessions`, {
122
+ method: 'POST',
123
+ headers: { 'content-type': 'application/json' },
124
+ body: '{}',
125
+ });
126
+ // A 4xx/5xx body is the Worker's { error } shape, not a session: surface
127
+ // it instead of connecting to ws_url=undefined.
128
+ if (!res.ok) {
129
+ log(`Session mint failed (HTTP ${res.status}); will retry.`);
130
+ return null;
131
+ }
132
+ const minted = (await res.json());
133
+ if (!minted.connection_token || !minted.ws_url) {
134
+ log('Session mint returned an unexpected response; will retry.');
135
+ return null;
136
+ }
137
+ // Re-mint shortly before the 12h connection-token TTL.
138
+ const stored = {
139
+ connection_token: minted.connection_token,
140
+ ws_url: minted.ws_url,
141
+ expires: Date.now() + 11 * 60 * 60 * 1000,
142
+ };
143
+ try {
144
+ localStorage.setItem(storageKey, JSON.stringify(stored));
145
+ }
146
+ catch {
147
+ /* storage disabled/full: fall back to an in-memory session */
148
+ }
149
+ log('Minted a new session (saved for reuse across reloads).');
150
+ return stored;
151
+ }
152
+ function scheduleReconnect() {
153
+ if (closed)
154
+ return;
155
+ attempt += 1;
156
+ const delay = Math.min(30_000, 1000 * 2 ** (attempt - 1));
157
+ setStatus('reconnecting');
158
+ log(`Reconnecting in ${Math.round(delay / 1000)}s (attempt ${attempt}).`);
159
+ reconnectTimer = setTimeout(() => void connect(), delay);
160
+ }
161
+ function handleInvoke(callId, toolName, args) {
162
+ log(`invoke ${toolName}(${JSON.stringify(args)})`);
163
+ const spec = listToolsRef.current().find((t) => t.name === toolName);
164
+ if (!spec) {
165
+ send({
166
+ type: 'result',
167
+ call_id: callId,
168
+ result: { error: `Unknown tool: ${toolName}` },
169
+ });
170
+ return;
171
+ }
172
+ if (!spec.mutation) {
173
+ void runAndRespond(callId, spec, args);
174
+ return;
175
+ }
176
+ // Mutation: hold the invoke and surface a confirmation prompt.
177
+ const now = new Date();
178
+ const prompt = {
179
+ callId,
180
+ toolName,
181
+ summary: spec.confirmationSummary?.(args) ?? toolName,
182
+ argsPreview: args,
183
+ createdAt: now.toISOString(),
184
+ expiresAt: new Date(now.getTime() + confirmationWindowSeconds * 1000).toISOString(),
185
+ };
186
+ heldRef.current.set(callId, { prompt, spec, args });
187
+ setPendingQueue((prev) => prev.some((p) => p.callId === callId) ? prev : [...prev, prompt]);
188
+ log(`awaiting confirmation for ${toolName}`);
189
+ // Self-expire so a never-answered prompt does not wedge the dialog.
190
+ setTimeout(() => {
191
+ if (!heldRef.current.has(callId))
192
+ return;
193
+ heldRef.current.delete(callId);
194
+ setPendingQueue((prev) => prev.filter((p) => p.callId !== callId));
195
+ log(`confirmation for ${toolName} expired`);
196
+ }, confirmationWindowSeconds * 1000);
197
+ }
198
+ async function connect() {
199
+ try {
200
+ acceptedRef.current = false;
201
+ let stored = loadStored();
202
+ if (stored) {
203
+ log('Reusing the saved session (token is stable across reloads).');
204
+ }
205
+ else {
206
+ stored = await mintSession();
207
+ if (!stored) {
208
+ scheduleReconnect();
209
+ return;
210
+ }
211
+ }
212
+ if (closed)
213
+ return;
214
+ setConnectionToken(stored.connection_token);
215
+ setStatus('ready');
216
+ const session = stored;
217
+ const ws = new WebSocket(session.ws_url);
218
+ wsRef.current = ws;
219
+ ws.onopen = () => {
220
+ ws.send(JSON.stringify({
221
+ type: 'hello',
222
+ tab_id: crypto.randomUUID(),
223
+ origin: location.origin,
224
+ url: location.href,
225
+ title,
226
+ connection_token: session.connection_token,
227
+ }));
228
+ };
229
+ ws.onmessage = (event) => {
230
+ let msg;
231
+ try {
232
+ msg = JSON.parse(event.data);
233
+ }
234
+ catch {
235
+ log('Ignored a malformed frame from the relay.');
236
+ return;
237
+ }
238
+ switch (msg['type']) {
239
+ case 'hello/accepted':
240
+ attempt = 0; // back-channel established: reset backoff
241
+ acceptedRef.current = true;
242
+ log('Back-channel connected; advertising tools.');
243
+ advertiseTools();
244
+ break;
245
+ case 'hello/rejected':
246
+ // Stale/invalid token: drop the saved session so the next
247
+ // reconnect mints a fresh one rather than looping on a bad token.
248
+ log('Session rejected by relay; clearing it and re-pairing.');
249
+ try {
250
+ localStorage.removeItem(storageKey);
251
+ }
252
+ catch {
253
+ /* storage disabled: nothing to clear */
254
+ }
255
+ break;
256
+ case 'harness_status':
257
+ setStatus(msg['attached'] ? 'connected' : 'ready');
258
+ break;
259
+ case 'invoke':
260
+ handleInvoke(msg['call_id'], msg['tool_name'] ?? '', msg['args'] ?? {});
261
+ break;
262
+ case 'ping':
263
+ send({ type: 'pong' });
264
+ break;
265
+ default:
266
+ break;
267
+ }
268
+ };
269
+ ws.onerror = () => {
270
+ log('WebSocket error; the connection will be retried.');
271
+ };
272
+ ws.onclose = () => {
273
+ acceptedRef.current = false;
274
+ if (!closed)
275
+ scheduleReconnect();
276
+ };
277
+ }
278
+ catch (err) {
279
+ log(`Error: ${err.message}`);
280
+ scheduleReconnect();
281
+ }
282
+ }
283
+ void connect();
284
+ return () => {
285
+ closed = true;
286
+ if (reconnectTimer)
287
+ clearTimeout(reconnectTimer);
288
+ wsRef.current?.close();
289
+ };
290
+ }, [
291
+ relayBaseUrl,
292
+ storageKey,
293
+ title,
294
+ confirmationWindowSeconds,
295
+ log,
296
+ send,
297
+ advertiseTools,
298
+ runAndRespond,
299
+ ]);
300
+ // Re-advertise when the registered tool set changes mid-connection.
301
+ const toolSignature = registry.tools.map((t) => t.name).join(',');
302
+ useEffect(() => {
303
+ if (acceptedRef.current) {
304
+ advertiseTools();
305
+ }
306
+ // oxlint-disable-next-line react/exhaustive-effect-dependencies -- `toolSignature` is a deliberate trigger-only dep; see the PR description.
307
+ }, [toolSignature, advertiseTools]);
308
+ return {
309
+ status,
310
+ connectionToken,
311
+ activity,
312
+ pendingConfirmation: pendingQueue[0] ?? null,
313
+ pendingQueueLength: pendingQueue.length,
314
+ approve,
315
+ deny,
316
+ };
317
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@r0hitsharma/webmcp",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "files": [
10
+ "dist",
11
+ "src"
12
+ ],
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.build.json",
21
+ "clean": "rm -rf dist",
22
+ "prepare": "npm run build",
23
+ "type:check": "tsc -p tsconfig.json --noEmit",
24
+ "lint": "oxlint -c oxlint.config.ts --max-warnings=0 src",
25
+ "lint:fix": "oxlint -c oxlint.config.ts --fix src",
26
+ "format": "oxfmt -c oxfmt.config.ts --write src",
27
+ "format:check": "oxfmt -c oxfmt.config.ts --check src"
28
+ },
29
+ "dependencies": {
30
+ "@mcp-b/global": "5.1.0"
31
+ },
32
+ "peerDependencies": {
33
+ "react": "^19.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@r0hitsharma/mcp-relay": "*",
37
+ "@r0hitsharma/oxfmt-config": "*",
38
+ "@r0hitsharma/oxlint-config": "*",
39
+ "@r0hitsharma/tsconfig": "*",
40
+ "@types/react": "^19.0.0",
41
+ "oxfmt": "0.67.0",
42
+ "oxlint": "1.82.0"
43
+ },
44
+ "version": "0.12.0-rohit-fork-ci.1",
45
+ "repository": {
46
+ "url": "https://github.com/r0hitsharma/uikit"
47
+ }
48
+ }