@assistant-ui/react-mcp 0.1.4 → 0.1.6

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.
@@ -1 +1 @@
1
- {"version":3,"file":"useMcpOAuthCallback.d.ts","names":[],"sources":["../../src/hooks/useMcpOAuthCallback.tsx"],"mappings":";;cAIa,8BACX,cACA,4BACC;KAWS;;EAEV;EACA,cAAc;EACd,WAAW,KAAK;;KAGN;EACV;EACA;EACA,OAAO;;iBAGO,oBACd,OAAM,6BACL;cAoDU,kBAAkB,GAC7B;EACE,YAAY,QAAQ,8BAA8B"}
1
+ {"version":3,"file":"useMcpOAuthCallback.d.ts","names":[],"sources":["../../src/hooks/useMcpOAuthCallback.tsx"],"mappings":";;cAkCa,8BACX,cACA,4BACC;KAWS;;EAEV;EACA,cAAc;EACd,WAAW,KAAK;;KAGN;EACV;EACA;EACA,OAAO;;iBAGO,oBACd,OAAM,6BACL;cA0DU,kBAAkB,GAC7B;EACE,YAAY,QAAQ,8BAA8B"}
@@ -4,6 +4,20 @@ import { c } from "@assistant-ui/tap/react-shim/compiler-runtime";
4
4
  import { useEffect, useRef, useState } from "@assistant-ui/tap/react-shim";
5
5
  import { Fragment, jsx } from "react/jsx-runtime";
6
6
  //#region src/hooks/useMcpOAuthCallback.tsx
7
+ const reportCallbackError = (name, error) => {
8
+ console.error(`[react-mcp] ${name} callback threw an error`, error);
9
+ };
10
+ const invokeMcpOAuthCallback = (name, callback, ...args) => {
11
+ if (!callback) return;
12
+ try {
13
+ const result = callback(...args);
14
+ if (result !== null && (typeof result === "object" || typeof result === "function") && "then" in result && typeof result.then === "function") Promise.resolve(result).catch((error) => {
15
+ reportCallbackError(name, error);
16
+ });
17
+ } catch (error) {
18
+ reportCallbackError(name, error);
19
+ }
20
+ };
7
21
  const createMcpOAuthCallbackError = (err, serverId) => {
8
22
  const message = err instanceof Error ? err.message : String(err);
9
23
  if (serverId) return new Error(`MCP OAuth callback for server "${serverId}" failed: ${message}`, { cause: err });
@@ -30,8 +44,8 @@ function useMcpOAuthCallback(opts = {}) {
30
44
  const parsed = new URL(url);
31
45
  const state = parsed.searchParams.get("state");
32
46
  if (state) serverId = decodeServerIdFromState(state);
33
- const error = parsed.searchParams.get("error");
34
- if (error) throw new Error(parsed.searchParams.get("error_description") ?? error);
47
+ const error_0 = parsed.searchParams.get("error");
48
+ if (error_0) throw new Error(parsed.searchParams.get("error_description") ?? error_0);
35
49
  if (!state) throw new Error("missing \"state\" parameter");
36
50
  if (!serverId) throw new Error("state was not created by assistant-ui MCP");
37
51
  setResult({
@@ -45,16 +59,17 @@ function useMcpOAuthCallback(opts = {}) {
45
59
  serverId,
46
60
  error: null
47
61
  });
48
- optsRef.current.onComplete?.(serverId);
49
62
  } catch (err) {
50
- const e = createMcpOAuthCallbackError(err, serverId);
63
+ const error = createMcpOAuthCallbackError(err, serverId);
51
64
  setResult({
52
65
  status: "error",
53
66
  serverId,
54
- error: e
67
+ error
55
68
  });
56
- optsRef.current.onError?.(e);
69
+ invokeMcpOAuthCallback("onError", optsRef.current.onError, error);
70
+ return;
57
71
  }
72
+ invokeMcpOAuthCallback("onComplete", optsRef.current.onComplete, serverId);
58
73
  })();
59
74
  }, [aui, opts.url]);
60
75
  return result;
@@ -1 +1 @@
1
- {"version":3,"file":"useMcpOAuthCallback.js","names":["FC","ReactNode","useEffect","useRef","useState","useAui","decodeServerIdFromState","createMcpOAuthCallbackError","err","serverId","Error","message","String","cause","UseMcpOAuthCallbackOptions","url","onComplete","onError","UseMcpOAuthCallbackResult","status","error","useMcpOAuthCallback","opts","aui","result","setResult","startedRef","optsRef","current","window","location","href","parsed","URL","state","searchParams","get","mcp","server","id","completeAuth","e","McpOAuthCallback","children","t0","$","_c","t1","t2"],"sources":["../../src/hooks/useMcpOAuthCallback.tsx"],"sourcesContent":["import { type FC, type ReactNode, useEffect, useRef, useState } from \"react\";\nimport { useAui } from \"@assistant-ui/store\";\nimport { decodeServerIdFromState } from \"../auth/createOAuthProvider\";\n\nexport const createMcpOAuthCallbackError = (\n err: unknown,\n serverId: string | null,\n): Error => {\n const message = err instanceof Error ? err.message : String(err);\n if (serverId) {\n return new Error(\n `MCP OAuth callback for server \"${serverId}\" failed: ${message}`,\n { cause: err },\n );\n }\n return new Error(`MCP OAuth callback failed: ${message}`, { cause: err });\n};\n\nexport type UseMcpOAuthCallbackOptions = {\n /** Defaults to `window.location.href`. */\n url?: string;\n onComplete?: (serverId: string) => void;\n onError?: (err: Error) => void;\n};\n\nexport type UseMcpOAuthCallbackResult = {\n status: \"idle\" | \"running\" | \"done\" | \"error\";\n serverId: string | null;\n error: Error | null;\n};\n\nexport function useMcpOAuthCallback(\n opts: UseMcpOAuthCallbackOptions = {},\n): UseMcpOAuthCallbackResult {\n const aui = useAui();\n const [result, setResult] = useState<UseMcpOAuthCallbackResult>({\n status: \"idle\",\n serverId: null,\n error: null,\n });\n // Guard against React 18 Strict Mode's mount-unmount-remount: the effect\n // body must not run completeAuth twice for the same URL, otherwise the\n // single-use OAuth code is double-redeemed and the second attempt 4xxs.\n const startedRef = useRef<string | null>(null);\n const optsRef = useRef(opts);\n optsRef.current = opts;\n\n useEffect(() => {\n const url =\n opts.url ?? (typeof window !== \"undefined\" ? window.location.href : null);\n if (!url) return;\n if (startedRef.current === url) return;\n startedRef.current = url;\n\n (async () => {\n let serverId: string | null = null;\n try {\n const parsed = new URL(url);\n const state = parsed.searchParams.get(\"state\");\n if (state) serverId = decodeServerIdFromState(state);\n const error = parsed.searchParams.get(\"error\");\n if (error) {\n throw new Error(\n parsed.searchParams.get(\"error_description\") ?? error,\n );\n }\n if (!state) throw new Error('missing \"state\" parameter');\n if (!serverId) {\n throw new Error(\"state was not created by assistant-ui MCP\");\n }\n setResult({ status: \"running\", serverId, error: null });\n await aui.mcp.server({ id: serverId }).completeAuth(url);\n setResult({ status: \"done\", serverId, error: null });\n optsRef.current.onComplete?.(serverId);\n } catch (err) {\n const e = createMcpOAuthCallbackError(err, serverId);\n setResult({ status: \"error\", serverId, error: e });\n optsRef.current.onError?.(e);\n }\n })();\n }, [aui, opts.url]);\n\n return result;\n}\n\nexport const McpOAuthCallback: FC<\n UseMcpOAuthCallbackOptions & {\n children?: (result: UseMcpOAuthCallbackResult) => ReactNode;\n }\n> = ({ children, ...opts }) => {\n const result = useMcpOAuthCallback(opts);\n if (children) return <>{children(result)}</>;\n return null;\n};\n"],"mappings":";;;;;;AAIA,MAAaO,+BACXC,KACAC,aACU;CACV,MAAME,UAAUH,eAAeE,QAAQF,IAAIG,UAAUC,OAAOJ,GAAG;CAC/D,IAAIC,UACF,OAAO,IAAIC,MACT,kCAAkCD,SAAQ,YAAaE,WACvD,EAAEE,OAAOL,IAAI,CACf;CAEF,OAAO,IAAIE,MAAM,8BAA8BC,WAAW,EAAEE,OAAOL,IAAI,CAAC;AAC1E;AAeA,SAAgBa,oBACdC,OAAmC,CAAC,GACT;CAC3B,MAAMC,MAAMlB,OAAO;CACnB,MAAM,CAACmB,QAAQC,aAAarB,SAAoC;EAC9De,QAAQ;EACRV,UAAU;EACVW,OAAO;CACT,CAAC;CAID,MAAMM,aAAavB,OAAsB,IAAI;CAC7C,MAAMwB,UAAUxB,OAAOmB,IAAI;CAC3BK,QAAQC,UAAUN;CAElBpB,gBAAgB;EACd,MAAMa,MACJO,KAAKP,QAAQ,OAAOc,WAAW,cAAcA,OAAOC,SAASC,OAAO;EACtE,IAAI,CAAChB,KAAK;EACV,IAAIW,WAAWE,YAAYb,KAAK;EAChCW,WAAWE,UAAUb;EAErB,CAAC,YAAY;GACX,IAAIN,WAA0B;GAC9B,IAAI;IACF,MAAMuB,SAAS,IAAIC,IAAIlB,GAAG;IAC1B,MAAMmB,QAAQF,OAAOG,aAAaC,IAAI,OAAO;IAC7C,IAAIF,OAAOzB,WAAWH,wBAAwB4B,KAAK;IACnD,MAAMd,QAAQY,OAAOG,aAAaC,IAAI,OAAO;IAC7C,IAAIhB,OACF,MAAM,IAAIV,MACRsB,OAAOG,aAAaC,IAAI,mBAAmB,KAAKhB,KAClD;IAEF,IAAI,CAACc,OAAO,MAAM,IAAIxB,MAAM,6BAA2B;IACvD,IAAI,CAACD,UACH,MAAM,IAAIC,MAAM,2CAA2C;IAE7De,UAAU;KAAEN,QAAQ;KAAWV;KAAUW,OAAO;IAAK,CAAC;IACtD,MAAMG,IAAIc,IAAIC,OAAO,EAAEC,IAAI9B,SAAS,CAAC,CAAC,CAAC+B,aAAazB,GAAG;IACvDU,UAAU;KAAEN,QAAQ;KAAQV;KAAUW,OAAO;IAAK,CAAC;IACnDO,QAAQC,QAAQZ,aAAaP,QAAQ;GACvC,SAASD,KAAK;IACZ,MAAMiC,IAAIlC,4BAA4BC,KAAKC,QAAQ;IACnDgB,UAAU;KAAEN,QAAQ;KAASV;KAAUW,OAAOqB;IAAE,CAAC;IACjDd,QAAQC,QAAQX,UAAUwB,CAAC;GAC7B;EACF,EAAA,CAAG;CACL,GAAG,CAAClB,KAAKD,KAAKP,GAAG,CAAC;CAElB,OAAOS;AACT;AAEA,MAAakB,oBAITE,OAAA;CAAA,MAAAC,IAAAC,EAAA,CAAA;CAAA,IAAAH;CAAA,IAAArB;CAAA,IAAAuB,EAAA,OAAAD,IAAA;EAAC,CAAA,CAAAD,aAAArB,QAAAsB;EAAqBC,EAAA,KAAAD;EAAAC,EAAA,KAAAF;EAAAE,EAAA,KAAAvB;CAAA,OAAA;EAAAqB,WAAAE,EAAA;EAAAvB,OAAAuB,EAAA;CAAA;CACxB,MAAArB,SAAeH,oBAAoBC,IAAI;CACvC,IAAIqB,UAAQ;EAAA,IAAAI;EAAA,IAAAF,EAAA,OAAAF,YAAAE,EAAA,OAAArB,QAAA;GAAYuB,KAAAJ,SAASnB,MAAM;GAACqB,EAAA,KAAAF;GAAAE,EAAA,KAAArB;GAAAqB,EAAA,KAAAE;EAAA,OAAAA,KAAAF,EAAA;EAAA,IAAAG;EAAA,IAAAH,EAAA,OAAAE,IAAA;GAAnBC,KAAA,oBAAA,UAAA,EAAA,UAAGD,GAAgB,CAAA;GAAIF,EAAA,KAAAE;GAAAF,EAAA,KAAAG;EAAA,OAAAA,KAAAH,EAAA;EAAA,OAAvBG;CAAuB;CAAC,OACtC;AAAI"}
1
+ {"version":3,"file":"useMcpOAuthCallback.js","names":["FC","ReactNode","useEffect","useRef","useState","useAui","decodeServerIdFromState","McpOAuthCallbackName","reportCallbackError","name","error","console","invokeMcpOAuthCallback","callback","args","TArgs","result","then","Promise","resolve","catch","createMcpOAuthCallbackError","err","serverId","Error","message","String","cause","UseMcpOAuthCallbackOptions","url","onComplete","onError","UseMcpOAuthCallbackResult","status","useMcpOAuthCallback","opts","aui","setResult","startedRef","optsRef","current","window","location","href","parsed","URL","state","searchParams","get","mcp","server","id","completeAuth","McpOAuthCallback","children","t0","$","_c","t1","t2"],"sources":["../../src/hooks/useMcpOAuthCallback.tsx"],"sourcesContent":["import { type FC, type ReactNode, useEffect, useRef, useState } from \"react\";\nimport { useAui } from \"@assistant-ui/store\";\nimport { decodeServerIdFromState } from \"../auth/createOAuthProvider\";\n\ntype McpOAuthCallbackName = \"onComplete\" | \"onError\";\n\nconst reportCallbackError = (name: McpOAuthCallbackName, error: unknown) => {\n console.error(`[react-mcp] ${name} callback threw an error`, error);\n};\n\nconst invokeMcpOAuthCallback = <TArgs extends unknown[]>(\n name: McpOAuthCallbackName,\n callback: ((...args: TArgs) => void) | undefined,\n ...args: TArgs\n) => {\n if (!callback) return;\n\n try {\n const result = callback(...args) as unknown;\n if (\n result !== null &&\n (typeof result === \"object\" || typeof result === \"function\") &&\n \"then\" in result &&\n typeof result.then === \"function\"\n ) {\n void Promise.resolve(result).catch((error) => {\n reportCallbackError(name, error);\n });\n }\n } catch (error) {\n reportCallbackError(name, error);\n }\n};\n\nexport const createMcpOAuthCallbackError = (\n err: unknown,\n serverId: string | null,\n): Error => {\n const message = err instanceof Error ? err.message : String(err);\n if (serverId) {\n return new Error(\n `MCP OAuth callback for server \"${serverId}\" failed: ${message}`,\n { cause: err },\n );\n }\n return new Error(`MCP OAuth callback failed: ${message}`, { cause: err });\n};\n\nexport type UseMcpOAuthCallbackOptions = {\n /** Defaults to `window.location.href`. */\n url?: string;\n onComplete?: (serverId: string) => void;\n onError?: (err: Error) => void;\n};\n\nexport type UseMcpOAuthCallbackResult = {\n status: \"idle\" | \"running\" | \"done\" | \"error\";\n serverId: string | null;\n error: Error | null;\n};\n\nexport function useMcpOAuthCallback(\n opts: UseMcpOAuthCallbackOptions = {},\n): UseMcpOAuthCallbackResult {\n const aui = useAui();\n const [result, setResult] = useState<UseMcpOAuthCallbackResult>({\n status: \"idle\",\n serverId: null,\n error: null,\n });\n // Guard against React 18 Strict Mode's mount-unmount-remount: the effect\n // body must not run completeAuth twice for the same URL, otherwise the\n // single-use OAuth code is double-redeemed and the second attempt 4xxs.\n const startedRef = useRef<string | null>(null);\n const optsRef = useRef(opts);\n optsRef.current = opts;\n\n useEffect(() => {\n const url =\n opts.url ?? (typeof window !== \"undefined\" ? window.location.href : null);\n if (!url) return;\n if (startedRef.current === url) return;\n startedRef.current = url;\n\n void (async () => {\n let serverId: string | null = null;\n try {\n const parsed = new URL(url);\n const state = parsed.searchParams.get(\"state\");\n if (state) serverId = decodeServerIdFromState(state);\n const error = parsed.searchParams.get(\"error\");\n if (error) {\n throw new Error(\n parsed.searchParams.get(\"error_description\") ?? error,\n );\n }\n if (!state) throw new Error('missing \"state\" parameter');\n if (!serverId) {\n throw new Error(\"state was not created by assistant-ui MCP\");\n }\n setResult({ status: \"running\", serverId, error: null });\n await aui.mcp.server({ id: serverId }).completeAuth(url);\n setResult({ status: \"done\", serverId, error: null });\n } catch (err) {\n const error = createMcpOAuthCallbackError(err, serverId);\n setResult({ status: \"error\", serverId, error });\n invokeMcpOAuthCallback(\"onError\", optsRef.current.onError, error);\n return;\n }\n\n invokeMcpOAuthCallback(\n \"onComplete\",\n optsRef.current.onComplete,\n serverId,\n );\n })();\n }, [aui, opts.url]);\n\n return result;\n}\n\nexport const McpOAuthCallback: FC<\n UseMcpOAuthCallbackOptions & {\n children?: (result: UseMcpOAuthCallbackResult) => ReactNode;\n }\n> = ({ children, ...opts }) => {\n const result = useMcpOAuthCallback(opts);\n if (children) return <>{children(result)}</>;\n return null;\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react-mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "MCP server configuration and connection primitives for @assistant-ui",
5
5
  "keywords": [
6
6
  "mcp",
@@ -30,10 +30,10 @@
30
30
  "@modelcontextprotocol/client": "^2.0.0",
31
31
  "@modelcontextprotocol/core": "^2.0.0",
32
32
  "@radix-ui/react-primitive": "^2.1.10",
33
- "@assistant-ui/core": "0.3.6",
34
- "@assistant-ui/store": "0.3.4",
35
- "@assistant-ui/tap": "0.9.10",
36
- "assistant-stream": "0.3.34"
33
+ "@assistant-ui/core": "0.3.7",
34
+ "@assistant-ui/store": "0.3.6",
35
+ "assistant-stream": "0.3.34",
36
+ "@assistant-ui/tap": "0.9.10"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@types/react": "*",
@@ -45,14 +45,17 @@
45
45
  }
46
46
  },
47
47
  "devDependencies": {
48
+ "@testing-library/dom": "^10.4.1",
49
+ "@testing-library/react": "^16.3.2",
48
50
  "@types/react": "^19.2.17",
51
+ "jsdom": "^29.1.1",
49
52
  "react": "^19.2.8",
50
53
  "vitest": "^4.1.10",
51
54
  "@assistant-ui/x-buildutils": "0.0.21"
52
55
  },
53
56
  "publishConfig": {
54
57
  "access": "public",
55
- "provenance": true
58
+ "provenance": false
56
59
  },
57
60
  "homepage": "https://www.assistant-ui.com/",
58
61
  "repository": {
@@ -1,5 +1,104 @@
1
- import { describe, expect, it } from "vitest";
2
- import { createMcpOAuthCallbackError } from "./useMcpOAuthCallback";
1
+ // @vitest-environment jsdom
2
+
3
+ import { renderHook, waitFor } from "@testing-library/react";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ const mocks = vi.hoisted(() => {
7
+ const completeAuth = vi.fn<(url: string) => Promise<void>>();
8
+ const server = vi.fn(() => ({ completeAuth }));
9
+ return {
10
+ completeAuth,
11
+ server,
12
+ aui: { mcp: { server } },
13
+ };
14
+ });
15
+
16
+ vi.mock("@assistant-ui/store", async (importOriginal) => ({
17
+ ...(await importOriginal()),
18
+ useAui: () => mocks.aui,
19
+ }));
20
+
21
+ const { createMcpOAuthCallbackError, useMcpOAuthCallback } =
22
+ await import("./useMcpOAuthCallback");
23
+
24
+ const callbackUrl =
25
+ "https://app.example.com/oauth/callback?state=aui-mcp%3AZG9jcw.nonce";
26
+
27
+ beforeEach(() => {
28
+ mocks.completeAuth.mockReset();
29
+ mocks.server.mockClear();
30
+ });
31
+
32
+ afterEach(() => {
33
+ vi.restoreAllMocks();
34
+ });
35
+
36
+ describe("useMcpOAuthCallback", () => {
37
+ it.each(["throws", "rejects"] as const)(
38
+ "keeps successful authentication done when onComplete %s",
39
+ async (mode) => {
40
+ const callbackError = new Error("telemetry failed");
41
+ const consoleError = vi
42
+ .spyOn(console, "error")
43
+ .mockImplementation(() => {});
44
+ const onComplete = vi.fn(() => {
45
+ if (mode === "throws") throw callbackError;
46
+ return Promise.reject(callbackError);
47
+ });
48
+ mocks.completeAuth.mockResolvedValueOnce();
49
+
50
+ const { result } = renderHook(() =>
51
+ useMcpOAuthCallback({ url: callbackUrl, onComplete }),
52
+ );
53
+
54
+ await waitFor(() => expect(result.current.status).toBe("done"));
55
+ expect(result.current).toEqual({
56
+ status: "done",
57
+ serverId: "docs",
58
+ error: null,
59
+ });
60
+ await waitFor(() => {
61
+ expect(consoleError).toHaveBeenCalledWith(
62
+ "[react-mcp] onComplete callback threw an error",
63
+ callbackError,
64
+ );
65
+ });
66
+ },
67
+ );
68
+
69
+ it.each(["throws", "rejects"] as const)(
70
+ "preserves authentication errors when onError %s",
71
+ async (mode) => {
72
+ const authError = new Error("invalid_grant");
73
+ const callbackError = new Error("telemetry failed");
74
+ const consoleError = vi
75
+ .spyOn(console, "error")
76
+ .mockImplementation(() => {});
77
+ const onError = vi.fn(() => {
78
+ if (mode === "throws") throw callbackError;
79
+ return Promise.reject(callbackError);
80
+ });
81
+ mocks.completeAuth.mockRejectedValueOnce(authError);
82
+
83
+ const { result } = renderHook(() =>
84
+ useMcpOAuthCallback({ url: callbackUrl, onError }),
85
+ );
86
+
87
+ await waitFor(() => expect(result.current.status).toBe("error"));
88
+ expect(result.current.serverId).toBe("docs");
89
+ expect(result.current.error).toMatchObject({
90
+ message: 'MCP OAuth callback for server "docs" failed: invalid_grant',
91
+ cause: authError,
92
+ });
93
+ await waitFor(() => {
94
+ expect(consoleError).toHaveBeenCalledWith(
95
+ "[react-mcp] onError callback threw an error",
96
+ callbackError,
97
+ );
98
+ });
99
+ },
100
+ );
101
+ });
3
102
 
4
103
  describe("createMcpOAuthCallbackError", () => {
5
104
  it("adds MCP OAuth callback context without a server id", () => {
@@ -2,6 +2,36 @@ import { type FC, type ReactNode, useEffect, useRef, useState } from "react";
2
2
  import { useAui } from "@assistant-ui/store";
3
3
  import { decodeServerIdFromState } from "../auth/createOAuthProvider";
4
4
 
5
+ type McpOAuthCallbackName = "onComplete" | "onError";
6
+
7
+ const reportCallbackError = (name: McpOAuthCallbackName, error: unknown) => {
8
+ console.error(`[react-mcp] ${name} callback threw an error`, error);
9
+ };
10
+
11
+ const invokeMcpOAuthCallback = <TArgs extends unknown[]>(
12
+ name: McpOAuthCallbackName,
13
+ callback: ((...args: TArgs) => void) | undefined,
14
+ ...args: TArgs
15
+ ) => {
16
+ if (!callback) return;
17
+
18
+ try {
19
+ const result = callback(...args) as unknown;
20
+ if (
21
+ result !== null &&
22
+ (typeof result === "object" || typeof result === "function") &&
23
+ "then" in result &&
24
+ typeof result.then === "function"
25
+ ) {
26
+ void Promise.resolve(result).catch((error) => {
27
+ reportCallbackError(name, error);
28
+ });
29
+ }
30
+ } catch (error) {
31
+ reportCallbackError(name, error);
32
+ }
33
+ };
34
+
5
35
  export const createMcpOAuthCallbackError = (
6
36
  err: unknown,
7
37
  serverId: string | null,
@@ -52,7 +82,7 @@ export function useMcpOAuthCallback(
52
82
  if (startedRef.current === url) return;
53
83
  startedRef.current = url;
54
84
 
55
- (async () => {
85
+ void (async () => {
56
86
  let serverId: string | null = null;
57
87
  try {
58
88
  const parsed = new URL(url);
@@ -71,12 +101,18 @@ export function useMcpOAuthCallback(
71
101
  setResult({ status: "running", serverId, error: null });
72
102
  await aui.mcp.server({ id: serverId }).completeAuth(url);
73
103
  setResult({ status: "done", serverId, error: null });
74
- optsRef.current.onComplete?.(serverId);
75
104
  } catch (err) {
76
- const e = createMcpOAuthCallbackError(err, serverId);
77
- setResult({ status: "error", serverId, error: e });
78
- optsRef.current.onError?.(e);
105
+ const error = createMcpOAuthCallbackError(err, serverId);
106
+ setResult({ status: "error", serverId, error });
107
+ invokeMcpOAuthCallback("onError", optsRef.current.onError, error);
108
+ return;
79
109
  }
110
+
111
+ invokeMcpOAuthCallback(
112
+ "onComplete",
113
+ optsRef.current.onComplete,
114
+ serverId,
115
+ );
80
116
  })();
81
117
  }, [aui, opts.url]);
82
118