@stigmer/react 0.0.77 → 0.0.79

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 (45) 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 +92 -13
  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 +54 -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/useMcpServerConnect.d.ts +15 -12
  24. package/mcp-server/useMcpServerConnect.d.ts.map +1 -1
  25. package/mcp-server/useMcpServerConnect.js +17 -17
  26. package/mcp-server/useMcpServerConnect.js.map +1 -1
  27. package/mcp-server/useMcpServerCredentials.d.ts +59 -7
  28. package/mcp-server/useMcpServerCredentials.d.ts.map +1 -1
  29. package/mcp-server/useMcpServerCredentials.js +37 -10
  30. package/mcp-server/useMcpServerCredentials.js.map +1 -1
  31. package/mcp-server/useMcpServerOAuthConnect.d.ts +82 -0
  32. package/mcp-server/useMcpServerOAuthConnect.d.ts.map +1 -0
  33. package/mcp-server/useMcpServerOAuthConnect.js +199 -0
  34. package/mcp-server/useMcpServerOAuthConnect.js.map +1 -0
  35. package/package.json +4 -4
  36. package/src/index.ts +9 -1
  37. package/src/mcp-server/McpServerConfigPanel.tsx +153 -3
  38. package/src/mcp-server/McpServerDetailView.tsx +231 -172
  39. package/src/mcp-server/McpServerPicker.tsx +40 -2
  40. package/src/mcp-server/OAuthCallbackHandler.tsx +239 -0
  41. package/src/mcp-server/index.ts +17 -1
  42. package/src/mcp-server/useMcpServerConnect.ts +25 -22
  43. package/src/mcp-server/useMcpServerCredentials.ts +86 -13
  44. package/src/mcp-server/useMcpServerOAuthConnect.ts +312 -0
  45. package/styles.css +1 -1
@@ -0,0 +1,239 @@
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
+ /** The authorization code returned by the OAuth provider. */
11
+ readonly code: string;
12
+ /** The opaque state token used to correlate the callback with the originating request. */
13
+ readonly state: string;
14
+ }
15
+
16
+ /** Props for {@link OAuthCallbackHandler}. */
17
+ export interface OAuthCallbackHandlerProps {
18
+ /**
19
+ * Fallback callback invoked when the page was opened as a regular
20
+ * navigation (no `window.opener`), meaning the popup `postMessage`
21
+ * path cannot be used.
22
+ *
23
+ * Receives the extracted `code` and `state` so the host application
24
+ * can complete the OAuth flow via its own routing logic.
25
+ *
26
+ * When omitted and `window.opener` is unavailable, the component
27
+ * renders an instructional message asking the user to close the tab.
28
+ */
29
+ readonly onFallback?: (params: OAuthCallbackParams) => void;
30
+ /** Additional CSS classes for the root container. */
31
+ readonly className?: string;
32
+ }
33
+
34
+ /**
35
+ * Lightweight component for OAuth callback pages.
36
+ *
37
+ * Render this component at the URL configured as your OAuth redirect
38
+ * URI (`STIGMER_OAUTH_REDIRECT_URI`). It extracts the `code` and
39
+ * `state` query parameters from the current URL, posts them back to
40
+ * the opener window via `window.postMessage`, and closes the popup.
41
+ *
42
+ * The component handles three scenarios:
43
+ * 1. **Popup with opener** (primary): posts the message and closes.
44
+ * 2. **No opener, `onFallback` provided**: calls the fallback with
45
+ * the extracted parameters so the host app can handle them.
46
+ * 3. **No opener, no fallback**: shows a message asking the user to
47
+ * return to the main window.
48
+ *
49
+ * Platform builders create a route in their application that renders
50
+ * this component:
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * // app/auth/oauth/callback/page.tsx (Next.js)
55
+ * import { OAuthCallbackHandler } from "@stigmer/react";
56
+ *
57
+ * export default function OAuthCallbackPage() {
58
+ * return <OAuthCallbackHandler />;
59
+ * }
60
+ * ```
61
+ */
62
+ export function OAuthCallbackHandler({
63
+ onFallback,
64
+ className,
65
+ }: OAuthCallbackHandlerProps) {
66
+ const [status, setStatus] = useState<"processing" | "done" | "no-opener" | "error">("processing");
67
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
68
+ const didRun = useRef(false);
69
+
70
+ useEffect(() => {
71
+ if (didRun.current) return;
72
+ didRun.current = true;
73
+
74
+ const params = new URLSearchParams(window.location.search);
75
+ const code = params.get("code");
76
+ const state = params.get("state");
77
+
78
+ const oauthError = params.get("error");
79
+ if (oauthError) {
80
+ const description = params.get("error_description") || oauthError;
81
+ setErrorMessage(`Authentication failed: ${description}`);
82
+ setStatus("error");
83
+ return;
84
+ }
85
+
86
+ if (!code || !state) {
87
+ setErrorMessage(
88
+ "Missing authorization code or state parameter. " +
89
+ "This page should only be reached via an OAuth redirect.",
90
+ );
91
+ setStatus("error");
92
+ return;
93
+ }
94
+
95
+ const opener = window.opener as Window | null;
96
+ if (opener && !opener.closed) {
97
+ const message: OAuthCallbackMessage = {
98
+ type: OAUTH_CALLBACK_MESSAGE_TYPE,
99
+ code,
100
+ state,
101
+ };
102
+
103
+ try {
104
+ opener.postMessage(message, window.location.origin);
105
+ setStatus("done");
106
+ window.close();
107
+ } catch {
108
+ setErrorMessage(
109
+ "Could not communicate with the parent window. " +
110
+ "Please close this tab and try again.",
111
+ );
112
+ setStatus("error");
113
+ }
114
+ return;
115
+ }
116
+
117
+ if (onFallback) {
118
+ onFallback({ code, state });
119
+ setStatus("done");
120
+ return;
121
+ }
122
+
123
+ setStatus("no-opener");
124
+ }, [onFallback]);
125
+
126
+ return (
127
+ <div
128
+ className={cn(
129
+ "flex min-h-[200px] items-center justify-center p-8",
130
+ className,
131
+ )}
132
+ >
133
+ <div className="max-w-sm text-center">
134
+ {status === "processing" && (
135
+ <>
136
+ <Spinner />
137
+ <p className="mt-3 text-sm text-muted-foreground">
138
+ Completing authentication...
139
+ </p>
140
+ </>
141
+ )}
142
+
143
+ {status === "done" && (
144
+ <p className="text-sm text-muted-foreground">
145
+ Authentication complete. You can close this window.
146
+ </p>
147
+ )}
148
+
149
+ {status === "no-opener" && (
150
+ <>
151
+ <CheckIcon />
152
+ <p className="mt-3 text-sm font-medium text-foreground">
153
+ Authentication successful
154
+ </p>
155
+ <p className="mt-1 text-xs text-muted-foreground">
156
+ Please close this tab and return to the application to continue.
157
+ </p>
158
+ </>
159
+ )}
160
+
161
+ {status === "error" && (
162
+ <>
163
+ <WarningIcon />
164
+ <p className="mt-3 text-sm font-medium text-destructive">
165
+ Authentication failed
166
+ </p>
167
+ {errorMessage && (
168
+ <p className="mt-1 text-xs text-muted-foreground">
169
+ {errorMessage}
170
+ </p>
171
+ )}
172
+ </>
173
+ )}
174
+ </div>
175
+ </div>
176
+ );
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // Icons -- inline SVGs (no external icon dependency, matches SDK pattern)
181
+ // ---------------------------------------------------------------------------
182
+
183
+ function Spinner() {
184
+ return (
185
+ <svg
186
+ width="24"
187
+ height="24"
188
+ viewBox="0 0 16 16"
189
+ fill="none"
190
+ stroke="currentColor"
191
+ strokeWidth="2"
192
+ strokeLinecap="round"
193
+ className="mx-auto animate-spin text-muted-foreground"
194
+ aria-hidden="true"
195
+ >
196
+ <path d="M8 2a6 6 0 1 0 6 6" />
197
+ </svg>
198
+ );
199
+ }
200
+
201
+ function CheckIcon() {
202
+ return (
203
+ <svg
204
+ width="24"
205
+ height="24"
206
+ viewBox="0 0 16 16"
207
+ fill="none"
208
+ stroke="currentColor"
209
+ strokeWidth="2"
210
+ strokeLinecap="round"
211
+ strokeLinejoin="round"
212
+ className="mx-auto text-success"
213
+ aria-hidden="true"
214
+ >
215
+ <path d="m3 8.5 3.5 3.5 6.5-8" />
216
+ </svg>
217
+ );
218
+ }
219
+
220
+ function WarningIcon() {
221
+ return (
222
+ <svg
223
+ width="24"
224
+ height="24"
225
+ viewBox="0 0 16 16"
226
+ fill="none"
227
+ stroke="currentColor"
228
+ strokeWidth="1.5"
229
+ strokeLinecap="round"
230
+ strokeLinejoin="round"
231
+ className="mx-auto text-destructive"
232
+ aria-hidden="true"
233
+ >
234
+ <path d="M8 1.5 1 14h14L8 1.5Z" />
235
+ <path d="M8 6v3.5" />
236
+ <circle cx="8" cy="11.5" r="0.5" fill="currentColor" stroke="none" />
237
+ </svg>
238
+ );
239
+ }
@@ -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";
@@ -6,6 +6,7 @@ import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/
6
6
  import { ConnectInputSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
7
7
  import type { EnvVarInput } from "@stigmer/sdk";
8
8
  import { useStigmer } from "../hooks";
9
+ import { resolveSystemEnvVarValues } from "../environment/systemEnvVars";
9
10
  import { toError } from "../internal/toError";
10
11
 
11
12
  /** Return value of {@link useMcpServerConnect}. */
@@ -18,13 +19,15 @@ export interface UseMcpServerConnectReturn {
18
19
  * agent-runner. The RPC blocks until the workflow completes
19
20
  * (typically 5-15 seconds, ~30s timeout).
20
21
  *
21
- * When `runtimeEnv` is provided, the values are sent directly to
22
- * the backend as one-time credentials (not persisted to any
23
- * environment). When omitted, the backend resolves credentials from
24
- * the authenticated user's personal environment.
22
+ * Platform system env vars (`STIGMER_SERVER_ADDRESS`,
23
+ * `STIGMER_API_KEY`) are always injected automatically from
24
+ * the current SDK context. When `runtimeEnv` is also provided,
25
+ * those values are merged on top (caller values win). The backend
26
+ * merges the result on top of the user's personal environment so
27
+ * saved credentials (e.g., OAuth tokens) are still resolved.
25
28
  *
26
29
  * @param mcpServerId - System-generated ID of the MCP server (metadata.id).
27
- * @param runtimeEnv - Optional one-time environment variables.
30
+ * @param runtimeEnv - Optional additional environment variables.
28
31
  * @returns The updated McpServer with populated status.discovered_capabilities
29
32
  * and status.tool_approvals.
30
33
  */
@@ -48,13 +51,14 @@ export interface UseMcpServerConnectReturn {
48
51
  * server's tools and resource templates, then classifies each tool's
49
52
  * approval policy via a structured-output LLM call.
50
53
  *
51
- * Supports two credential modes:
52
- * - **Saved credentials** (default): Credentials are pre-saved to the
53
- * user's personal environment, then `connect(id)` is called without
54
- * `runtimeEnv`. The backend resolves them automatically.
55
- * - **One-time use**: `connect(id, runtimeEnv)` passes credentials
56
- * directly to the backend. They are used for this connect only and
57
- * not persisted.
54
+ * Platform system env vars (`STIGMER_SERVER_ADDRESS`,
55
+ * `STIGMER_API_KEY`) are always injected from the current SDK
56
+ * context. The backend merges these on top of the caller's personal
57
+ * environment, so saved credentials (e.g., OAuth tokens) are still
58
+ * resolved while platform addresses are always up to date.
59
+ *
60
+ * Additional one-time credentials can be passed via `runtimeEnv`
61
+ * and will override both system vars and personal env values.
58
62
  *
59
63
  * @example
60
64
  * ```tsx
@@ -90,21 +94,20 @@ export function useMcpServerConnect(): UseMcpServerConnectReturn {
90
94
  setError(null);
91
95
 
92
96
  try {
97
+ const systemEnv = await resolveSystemEnvVarValues(stigmer);
98
+ const mergedEnv = { ...systemEnv, ...(runtimeEnv ?? {}) };
99
+
93
100
  const runtimeEnvMap: Record<string, { value: string; isSecret: boolean }> = {};
94
- if (runtimeEnv) {
95
- for (const [key, input] of Object.entries(runtimeEnv)) {
96
- runtimeEnvMap[key] = {
97
- value: input.value,
98
- isSecret: input.isSecret ?? false,
99
- };
100
- }
101
+ for (const [key, envInput] of Object.entries(mergedEnv)) {
102
+ runtimeEnvMap[key] = {
103
+ value: envInput.value,
104
+ isSecret: envInput.isSecret ?? false,
105
+ };
101
106
  }
102
107
 
103
108
  const input = create(ConnectInputSchema, {
104
109
  mcpServerId,
105
- ...(Object.keys(runtimeEnvMap).length > 0 && {
106
- runtimeEnv: runtimeEnvMap,
107
- }),
110
+ runtimeEnv: runtimeEnvMap,
108
111
  });
109
112
 
110
113
  return await stigmer.mcpServer.connect(input);
@@ -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,