@stigmer/react 0.0.76 → 0.0.78

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.
Files changed (40) hide show
  1. package/index.d.ts +2 -2
  2. package/index.d.ts.map +1 -1
  3. package/index.js +2 -2
  4. package/index.js.map +1 -1
  5. package/mcp-server/McpServerConfigPanel.d.ts +28 -1
  6. package/mcp-server/McpServerConfigPanel.d.ts.map +1 -1
  7. package/mcp-server/McpServerConfigPanel.js +23 -2
  8. package/mcp-server/McpServerConfigPanel.js.map +1 -1
  9. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  10. package/mcp-server/McpServerDetailView.js +96 -9
  11. package/mcp-server/McpServerDetailView.js.map +1 -1
  12. package/mcp-server/McpServerPicker.d.ts.map +1 -1
  13. package/mcp-server/McpServerPicker.js +34 -2
  14. package/mcp-server/McpServerPicker.js.map +1 -1
  15. package/mcp-server/OAuthCallbackHandler.d.ts +52 -0
  16. package/mcp-server/OAuthCallbackHandler.d.ts.map +1 -0
  17. package/mcp-server/OAuthCallbackHandler.js +98 -0
  18. package/mcp-server/OAuthCallbackHandler.js.map +1 -0
  19. package/mcp-server/index.d.ts +6 -2
  20. package/mcp-server/index.d.ts.map +1 -1
  21. package/mcp-server/index.js +2 -0
  22. package/mcp-server/index.js.map +1 -1
  23. package/mcp-server/useMcpServerCredentials.d.ts +59 -7
  24. package/mcp-server/useMcpServerCredentials.d.ts.map +1 -1
  25. package/mcp-server/useMcpServerCredentials.js +37 -10
  26. package/mcp-server/useMcpServerCredentials.js.map +1 -1
  27. package/mcp-server/useMcpServerOAuthConnect.d.ts +81 -0
  28. package/mcp-server/useMcpServerOAuthConnect.d.ts.map +1 -0
  29. package/mcp-server/useMcpServerOAuthConnect.js +187 -0
  30. package/mcp-server/useMcpServerOAuthConnect.js.map +1 -0
  31. package/package.json +4 -4
  32. package/src/index.ts +9 -1
  33. package/src/mcp-server/McpServerConfigPanel.tsx +153 -3
  34. package/src/mcp-server/McpServerDetailView.tsx +283 -97
  35. package/src/mcp-server/McpServerPicker.tsx +40 -2
  36. package/src/mcp-server/OAuthCallbackHandler.tsx +237 -0
  37. package/src/mcp-server/index.ts +17 -1
  38. package/src/mcp-server/useMcpServerCredentials.ts +86 -13
  39. package/src/mcp-server/useMcpServerOAuthConnect.ts +298 -0
  40. package/styles.css +1 -1
@@ -0,0 +1,237 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { cn } from "@stigmer/theme";
5
+ import { OAUTH_CALLBACK_MESSAGE_TYPE } from "./useMcpServerOAuthConnect";
6
+ import type { OAuthCallbackMessage } from "./useMcpServerOAuthConnect";
7
+
8
+ /** Parameters extracted from the OAuth callback URL. */
9
+ export interface OAuthCallbackParams {
10
+ readonly code: string;
11
+ readonly state: string;
12
+ }
13
+
14
+ /** Props for {@link OAuthCallbackHandler}. */
15
+ export interface OAuthCallbackHandlerProps {
16
+ /**
17
+ * Fallback callback invoked when the page was opened as a regular
18
+ * navigation (no `window.opener`), meaning the popup `postMessage`
19
+ * path cannot be used.
20
+ *
21
+ * Receives the extracted `code` and `state` so the host application
22
+ * can complete the OAuth flow via its own routing logic.
23
+ *
24
+ * When omitted and `window.opener` is unavailable, the component
25
+ * renders an instructional message asking the user to close the tab.
26
+ */
27
+ readonly onFallback?: (params: OAuthCallbackParams) => void;
28
+ /** Additional CSS classes for the root container. */
29
+ readonly className?: string;
30
+ }
31
+
32
+ /**
33
+ * Lightweight component for OAuth callback pages.
34
+ *
35
+ * Render this component at the URL configured as your OAuth redirect
36
+ * URI (`STIGMER_OAUTH_REDIRECT_URI`). It extracts the `code` and
37
+ * `state` query parameters from the current URL, posts them back to
38
+ * the opener window via `window.postMessage`, and closes the popup.
39
+ *
40
+ * The component handles three scenarios:
41
+ * 1. **Popup with opener** (primary): posts the message and closes.
42
+ * 2. **No opener, `onFallback` provided**: calls the fallback with
43
+ * the extracted parameters so the host app can handle them.
44
+ * 3. **No opener, no fallback**: shows a message asking the user to
45
+ * return to the main window.
46
+ *
47
+ * Platform builders create a route in their application that renders
48
+ * this component:
49
+ *
50
+ * @example
51
+ * ```tsx
52
+ * // app/auth/oauth/callback/page.tsx (Next.js)
53
+ * import { OAuthCallbackHandler } from "@stigmer/react";
54
+ *
55
+ * export default function OAuthCallbackPage() {
56
+ * return <OAuthCallbackHandler />;
57
+ * }
58
+ * ```
59
+ */
60
+ export function OAuthCallbackHandler({
61
+ onFallback,
62
+ className,
63
+ }: OAuthCallbackHandlerProps) {
64
+ const [status, setStatus] = useState<"processing" | "done" | "no-opener" | "error">("processing");
65
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
66
+ const didRun = useRef(false);
67
+
68
+ useEffect(() => {
69
+ if (didRun.current) return;
70
+ didRun.current = true;
71
+
72
+ const params = new URLSearchParams(window.location.search);
73
+ const code = params.get("code");
74
+ const state = params.get("state");
75
+
76
+ const oauthError = params.get("error");
77
+ if (oauthError) {
78
+ const description = params.get("error_description") || oauthError;
79
+ setErrorMessage(`Authentication failed: ${description}`);
80
+ setStatus("error");
81
+ return;
82
+ }
83
+
84
+ if (!code || !state) {
85
+ setErrorMessage(
86
+ "Missing authorization code or state parameter. " +
87
+ "This page should only be reached via an OAuth redirect.",
88
+ );
89
+ setStatus("error");
90
+ return;
91
+ }
92
+
93
+ const opener = window.opener as Window | null;
94
+ if (opener && !opener.closed) {
95
+ const message: OAuthCallbackMessage = {
96
+ type: OAUTH_CALLBACK_MESSAGE_TYPE,
97
+ code,
98
+ state,
99
+ };
100
+
101
+ try {
102
+ opener.postMessage(message, window.location.origin);
103
+ setStatus("done");
104
+ window.close();
105
+ } catch {
106
+ setErrorMessage(
107
+ "Could not communicate with the parent window. " +
108
+ "Please close this tab and try again.",
109
+ );
110
+ setStatus("error");
111
+ }
112
+ return;
113
+ }
114
+
115
+ if (onFallback) {
116
+ onFallback({ code, state });
117
+ setStatus("done");
118
+ return;
119
+ }
120
+
121
+ setStatus("no-opener");
122
+ }, [onFallback]);
123
+
124
+ return (
125
+ <div
126
+ className={cn(
127
+ "flex min-h-[200px] items-center justify-center p-8",
128
+ className,
129
+ )}
130
+ >
131
+ <div className="max-w-sm text-center">
132
+ {status === "processing" && (
133
+ <>
134
+ <Spinner />
135
+ <p className="mt-3 text-sm text-muted-foreground">
136
+ Completing authentication...
137
+ </p>
138
+ </>
139
+ )}
140
+
141
+ {status === "done" && (
142
+ <p className="text-sm text-muted-foreground">
143
+ Authentication complete. You can close this window.
144
+ </p>
145
+ )}
146
+
147
+ {status === "no-opener" && (
148
+ <>
149
+ <CheckIcon />
150
+ <p className="mt-3 text-sm font-medium text-foreground">
151
+ Authentication successful
152
+ </p>
153
+ <p className="mt-1 text-xs text-muted-foreground">
154
+ Please close this tab and return to the application to continue.
155
+ </p>
156
+ </>
157
+ )}
158
+
159
+ {status === "error" && (
160
+ <>
161
+ <WarningIcon />
162
+ <p className="mt-3 text-sm font-medium text-destructive">
163
+ Authentication failed
164
+ </p>
165
+ {errorMessage && (
166
+ <p className="mt-1 text-xs text-muted-foreground">
167
+ {errorMessage}
168
+ </p>
169
+ )}
170
+ </>
171
+ )}
172
+ </div>
173
+ </div>
174
+ );
175
+ }
176
+
177
+ // ---------------------------------------------------------------------------
178
+ // Icons -- inline SVGs (no external icon dependency, matches SDK pattern)
179
+ // ---------------------------------------------------------------------------
180
+
181
+ function Spinner() {
182
+ return (
183
+ <svg
184
+ width="24"
185
+ height="24"
186
+ viewBox="0 0 16 16"
187
+ fill="none"
188
+ stroke="currentColor"
189
+ strokeWidth="2"
190
+ strokeLinecap="round"
191
+ className="mx-auto animate-spin text-muted-foreground"
192
+ aria-hidden="true"
193
+ >
194
+ <path d="M8 2a6 6 0 1 0 6 6" />
195
+ </svg>
196
+ );
197
+ }
198
+
199
+ function CheckIcon() {
200
+ return (
201
+ <svg
202
+ width="24"
203
+ height="24"
204
+ viewBox="0 0 16 16"
205
+ fill="none"
206
+ stroke="currentColor"
207
+ strokeWidth="2"
208
+ strokeLinecap="round"
209
+ strokeLinejoin="round"
210
+ className="mx-auto text-success"
211
+ aria-hidden="true"
212
+ >
213
+ <path d="m3 8.5 3.5 3.5 6.5-8" />
214
+ </svg>
215
+ );
216
+ }
217
+
218
+ function WarningIcon() {
219
+ return (
220
+ <svg
221
+ width="24"
222
+ height="24"
223
+ viewBox="0 0 16 16"
224
+ fill="none"
225
+ stroke="currentColor"
226
+ strokeWidth="1.5"
227
+ strokeLinecap="round"
228
+ strokeLinejoin="round"
229
+ className="mx-auto text-destructive"
230
+ aria-hidden="true"
231
+ >
232
+ <path d="M8 1.5 1 14h14L8 1.5Z" />
233
+ <path d="M8 6v3.5" />
234
+ <circle cx="8" cy="11.5" r="0.5" fill="currentColor" stroke="none" />
235
+ </svg>
236
+ );
237
+ }
@@ -32,6 +32,7 @@ export { McpServerConfigPanel } from "./McpServerConfigPanel";
32
32
  export type {
33
33
  McpServerConfigPanelProps,
34
34
  McpServerCredentialsProps,
35
+ McpServerOAuthSignInProps,
35
36
  } from "./McpServerConfigPanel";
36
37
 
37
38
  export { useMcpServerSetup, toServerKey } from "./useMcpServerSetup";
@@ -52,5 +53,20 @@ export type {
52
53
  export { useMcpServerConnect } from "./useMcpServerConnect";
53
54
  export type { UseMcpServerConnectReturn } from "./useMcpServerConnect";
54
55
 
56
+ export { useMcpServerOAuthConnect } from "./useMcpServerOAuthConnect";
57
+ export type {
58
+ UseMcpServerOAuthConnectReturn,
59
+ OAuthConnectPhase,
60
+ } from "./useMcpServerOAuthConnect";
61
+
62
+ export { OAuthCallbackHandler } from "./OAuthCallbackHandler";
63
+ export type {
64
+ OAuthCallbackHandlerProps,
65
+ OAuthCallbackParams,
66
+ } from "./OAuthCallbackHandler";
67
+
55
68
  export { useMcpServerCredentials } from "./useMcpServerCredentials";
56
- export type { UseMcpServerCredentialsReturn } from "./useMcpServerCredentials";
69
+ export type {
70
+ UseMcpServerCredentialsReturn,
71
+ McpServerAuthMode,
72
+ } from "./useMcpServerCredentials";
@@ -8,17 +8,59 @@ import { diffEnvSpec } from "../environment/diffEnvSpec";
8
8
  import { SYSTEM_ENV_VAR_KEYS } from "../environment/systemEnvVars";
9
9
  import type { EnvVarFormVariable } from "../environment/EnvVarForm";
10
10
 
11
+ /**
12
+ * Credential acquisition mode for an MCP server.
13
+ *
14
+ * - `"manual"` — all env vars are entered by the user via a form.
15
+ * - `"oauth"` — at least one env var (`target_env_var`) is acquired
16
+ * via OAuth. Additional manual vars may still be required (mixed mode).
17
+ */
18
+ export type McpServerAuthMode = "manual" | "oauth";
19
+
11
20
  /** Return value of {@link useMcpServerCredentials}. */
12
21
  export interface UseMcpServerCredentialsReturn {
22
+ /**
23
+ * Credential acquisition mode derived from `spec.auth`.
24
+ *
25
+ * - `"manual"` when `spec.auth` is absent: all env vars are user-entered.
26
+ * - `"oauth"` when `spec.auth` is present: the `target_env_var` is
27
+ * acquired via OAuth. Check {@link missingVariables} for any
28
+ * additional manual vars that are also needed (mixed mode).
29
+ */
30
+ readonly authMode: McpServerAuthMode;
31
+ /**
32
+ * The env var name managed by OAuth, or `null` when `authMode` is `"manual"`.
33
+ * Corresponds to `spec.auth.target_env_var`.
34
+ */
35
+ readonly oauthTargetEnvVar: string | null;
36
+ /**
37
+ * `true` when the OAuth-managed env var exists in the personal
38
+ * environment. Always `false` when `authMode` is `"manual"`.
39
+ */
40
+ readonly isOAuthConnected: boolean;
41
+ /**
42
+ * Informational hint about expected token lifetime, or `null`.
43
+ * Sourced from `spec.auth.token_lifetime_hint`.
44
+ */
45
+ readonly tokenLifetimeHint: string | null;
13
46
  /**
14
47
  * Variables required by the MCP server that are missing from the
15
48
  * user's personal environment. Empty when all variables are present
16
49
  * or the server has no `env_spec`.
17
50
  *
51
+ * When `authMode` is `"oauth"`, the OAuth-managed `target_env_var`
52
+ * is excluded from this list — it is acquired via the OAuth flow,
53
+ * not via a manual form. Only additional non-OAuth vars appear here.
54
+ *
18
55
  * Suitable as direct input to {@link EnvVarForm}.
19
56
  */
20
57
  readonly missingVariables: EnvVarFormVariable[];
21
- /** `true` when all required credentials are available. */
58
+ /**
59
+ * `true` when all required credentials are available — both
60
+ * OAuth-managed and manual variables. For OAuth servers this means
61
+ * the OAuth token is in the personal env AND any additional manual
62
+ * vars are also present.
63
+ */
22
64
  readonly isReady: boolean;
23
65
  /** `true` while the personal environment is being fetched. */
24
66
  readonly isLoading: boolean;
@@ -47,6 +89,12 @@ export interface UseMcpServerCredentialsReturn {
47
89
  * computes the missing set and exposes `saveCredentials` to persist
48
90
  * them.
49
91
  *
92
+ * **Auth-mode-aware**: when `spec.auth` is configured, the hook
93
+ * identifies the OAuth-managed variable (`target_env_var`) and
94
+ * excludes it from `missingVariables` — that variable is acquired
95
+ * via {@link useMcpServerOAuthConnect}, not a manual form. Additional
96
+ * non-OAuth vars still appear in `missingVariables` (mixed mode).
97
+ *
50
98
  * Unlike {@link useMcpServerSetup} which manages multi-server setup
51
99
  * for session creation, this hook is scoped to a single server and
52
100
  * always persists to the personal environment (no one-time option).
@@ -55,15 +103,20 @@ export interface UseMcpServerCredentialsReturn {
55
103
  *
56
104
  * @example
57
105
  * ```tsx
58
- * const { missingVariables, isReady, saveCredentials, isSaving } =
59
- * useMcpServerCredentials("acme", mcpServer);
106
+ * const creds = useMcpServerCredentials("acme", mcpServer);
60
107
  *
61
- * if (!isReady) {
108
+ * // OAuth server — sign-in button + optional manual form
109
+ * if (creds.authMode === "oauth" && !creds.isOAuthConnected) {
110
+ * return <button onClick={startOAuth}>Sign in</button>;
111
+ * }
112
+ *
113
+ * // Manual vars still needed (mixed mode or manual-only)
114
+ * if (creds.missingVariables.length > 0) {
62
115
  * return (
63
116
  * <EnvVarForm
64
- * variables={missingVariables}
65
- * onSubmit={(values) => saveCredentials(values)}
66
- * isSubmitting={isSaving}
117
+ * variables={creds.missingVariables}
118
+ * onSubmit={(values) => creds.saveCredentials(values)}
119
+ * isSubmitting={creds.isSaving}
67
120
  * hideSaveToggle
68
121
  * />
69
122
  * );
@@ -76,21 +129,37 @@ export function useMcpServerCredentials(
76
129
  ): UseMcpServerCredentialsReturn {
77
130
  const personalEnv = usePersonalEnvironment(org);
78
131
 
79
- const missingVariables = useMemo(() => {
132
+ const auth = mcpServer?.spec?.auth;
133
+ const authMode: McpServerAuthMode = auth ? "oauth" : "manual";
134
+ const oauthTargetEnvVar = auth?.targetEnvVar || null;
135
+ const tokenLifetimeHint = auth?.tokenLifetimeHint || null;
136
+
137
+ const existingKeys = useMemo(
138
+ () => new Set(Object.keys(personalEnv.environment?.spec?.data ?? {})),
139
+ [personalEnv.environment],
140
+ );
141
+
142
+ const isOAuthConnected = authMode === "oauth"
143
+ && oauthTargetEnvVar !== null
144
+ && existingKeys.has(oauthTargetEnvVar);
145
+
146
+ const allMissingVariables = useMemo(() => {
80
147
  if (!mcpServer) return [];
81
148
  const envSpecData = mcpServer.spec?.envSpec?.data;
82
149
  if (!envSpecData || Object.keys(envSpecData).length === 0) return [];
83
150
 
84
- const existingKeys = new Set(
85
- Object.keys(personalEnv.environment?.spec?.data ?? {}),
86
- );
87
151
  return diffEnvSpec(envSpecData, existingKeys).filter(
88
152
  (v) => !SYSTEM_ENV_VAR_KEYS.has(v.key),
89
153
  );
90
- }, [mcpServer, personalEnv.environment]);
154
+ }, [mcpServer, existingKeys]);
155
+
156
+ const missingVariables = useMemo(() => {
157
+ if (!oauthTargetEnvVar) return allMissingVariables;
158
+ return allMissingVariables.filter((v) => v.key !== oauthTargetEnvVar);
159
+ }, [allMissingVariables, oauthTargetEnvVar]);
91
160
 
92
161
  const isReady =
93
- !personalEnv.isLoading && missingVariables.length === 0;
162
+ !personalEnv.isLoading && allMissingVariables.length === 0;
94
163
 
95
164
  const saveCredentials = useCallback(
96
165
  async (values: Record<string, EnvVarInput>): Promise<void> => {
@@ -101,6 +170,10 @@ export function useMcpServerCredentials(
101
170
  );
102
171
 
103
172
  return {
173
+ authMode,
174
+ oauthTargetEnvVar,
175
+ isOAuthConnected,
176
+ tokenLifetimeHint,
104
177
  missingVariables,
105
178
  isReady,
106
179
  isLoading: personalEnv.isLoading,