@stigmer/react 0.0.78 → 0.0.80

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 (99) hide show
  1. package/agent/AgentDetailView.js +2 -2
  2. package/agent/AgentDetailView.js.map +1 -1
  3. package/agent/agentSetupReducer.d.ts +3 -3
  4. package/agent/agentSetupReducer.d.ts.map +1 -1
  5. package/agent/index.d.ts +1 -1
  6. package/agent/index.d.ts.map +1 -1
  7. package/agent/index.js +1 -1
  8. package/agent/index.js.map +1 -1
  9. package/agent/useAgentSetup.d.ts +4 -4
  10. package/agent/useAgentSetup.js +9 -9
  11. package/agent/useAgentSetup.js.map +1 -1
  12. package/environment/EnvVarForm.d.ts +9 -3
  13. package/environment/EnvVarForm.d.ts.map +1 -1
  14. package/environment/EnvVarForm.js +1 -1
  15. package/environment/EnvVarForm.js.map +1 -1
  16. package/environment/EnvironmentListPanel.d.ts +19 -4
  17. package/environment/EnvironmentListPanel.d.ts.map +1 -1
  18. package/environment/EnvironmentListPanel.js +7 -3
  19. package/environment/EnvironmentListPanel.js.map +1 -1
  20. package/environment/{diffEnvSpec.d.ts → diffEnv.d.ts} +9 -8
  21. package/environment/diffEnv.d.ts.map +1 -0
  22. package/environment/{diffEnvSpec.js → diffEnv.js} +10 -9
  23. package/environment/diffEnv.js.map +1 -0
  24. package/environment/index.d.ts +1 -1
  25. package/environment/index.d.ts.map +1 -1
  26. package/environment/index.js +1 -1
  27. package/environment/index.js.map +1 -1
  28. package/execution/SessionVariablesInput.d.ts +1 -1
  29. package/index.d.ts +3 -3
  30. package/index.d.ts.map +1 -1
  31. package/index.js +3 -3
  32. package/index.js.map +1 -1
  33. package/library/parse-resource-yaml.d.ts +1 -1
  34. package/library/parse-resource-yaml.d.ts.map +1 -1
  35. package/library/parse-resource-yaml.js +26 -16
  36. package/library/parse-resource-yaml.js.map +1 -1
  37. package/library/serialize-resource-yaml.js +17 -21
  38. package/library/serialize-resource-yaml.js.map +1 -1
  39. package/mcp-server/McpServerDetailView.d.ts +8 -1
  40. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  41. package/mcp-server/McpServerDetailView.js +27 -6
  42. package/mcp-server/McpServerDetailView.js.map +1 -1
  43. package/mcp-server/McpServerPicker.d.ts +8 -1
  44. package/mcp-server/McpServerPicker.d.ts.map +1 -1
  45. package/mcp-server/McpServerPicker.js +2 -2
  46. package/mcp-server/McpServerPicker.js.map +1 -1
  47. package/mcp-server/OAuthCallbackHandler.d.ts +2 -0
  48. package/mcp-server/OAuthCallbackHandler.d.ts.map +1 -1
  49. package/mcp-server/OAuthCallbackHandler.js.map +1 -1
  50. package/mcp-server/index.d.ts +2 -0
  51. package/mcp-server/index.d.ts.map +1 -1
  52. package/mcp-server/index.js +1 -0
  53. package/mcp-server/index.js.map +1 -1
  54. package/mcp-server/mcpServerSetupReducer.d.ts +4 -4
  55. package/mcp-server/useMcpServerConnect.d.ts +15 -12
  56. package/mcp-server/useMcpServerConnect.d.ts.map +1 -1
  57. package/mcp-server/useMcpServerConnect.js +17 -17
  58. package/mcp-server/useMcpServerConnect.js.map +1 -1
  59. package/mcp-server/useMcpServerCredentials.d.ts +32 -17
  60. package/mcp-server/useMcpServerCredentials.d.ts.map +1 -1
  61. package/mcp-server/useMcpServerCredentials.js +30 -19
  62. package/mcp-server/useMcpServerCredentials.js.map +1 -1
  63. package/mcp-server/useMcpServerOAuthConnect.d.ts +4 -3
  64. package/mcp-server/useMcpServerOAuthConnect.d.ts.map +1 -1
  65. package/mcp-server/useMcpServerOAuthConnect.js +16 -4
  66. package/mcp-server/useMcpServerOAuthConnect.js.map +1 -1
  67. package/mcp-server/useMcpServerSetup.d.ts +5 -5
  68. package/mcp-server/useMcpServerSetup.d.ts.map +1 -1
  69. package/mcp-server/useMcpServerSetup.js +33 -13
  70. package/mcp-server/useMcpServerSetup.js.map +1 -1
  71. package/mcp-server/useOAuthGrantStatus.d.ts +41 -0
  72. package/mcp-server/useOAuthGrantStatus.d.ts.map +1 -0
  73. package/mcp-server/useOAuthGrantStatus.js +91 -0
  74. package/mcp-server/useOAuthGrantStatus.js.map +1 -0
  75. package/package.json +4 -4
  76. package/src/agent/AgentDetailView.tsx +5 -5
  77. package/src/agent/agentSetupReducer.ts +3 -3
  78. package/src/agent/index.ts +1 -1
  79. package/src/agent/useAgentSetup.ts +11 -11
  80. package/src/environment/EnvVarForm.tsx +9 -3
  81. package/src/environment/EnvironmentListPanel.tsx +27 -9
  82. package/src/environment/{diffEnvSpec.ts → diffEnv.ts} +10 -9
  83. package/src/environment/index.ts +1 -1
  84. package/src/execution/SessionVariablesInput.tsx +1 -1
  85. package/src/index.ts +4 -2
  86. package/src/library/parse-resource-yaml.ts +27 -18
  87. package/src/library/serialize-resource-yaml.ts +20 -27
  88. package/src/mcp-server/McpServerDetailView.tsx +39 -7
  89. package/src/mcp-server/McpServerPicker.tsx +9 -1
  90. package/src/mcp-server/OAuthCallbackHandler.tsx +2 -0
  91. package/src/mcp-server/index.ts +3 -0
  92. package/src/mcp-server/mcpServerSetupReducer.ts +4 -4
  93. package/src/mcp-server/useMcpServerConnect.ts +25 -22
  94. package/src/mcp-server/useMcpServerCredentials.ts +65 -32
  95. package/src/mcp-server/useMcpServerOAuthConnect.ts +20 -6
  96. package/src/mcp-server/useMcpServerSetup.ts +38 -15
  97. package/src/mcp-server/useOAuthGrantStatus.ts +125 -0
  98. package/environment/diffEnvSpec.d.ts.map +0 -1
  99. package/environment/diffEnvSpec.js.map +0 -1
@@ -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);
@@ -4,9 +4,10 @@ import { useCallback, useMemo } from "react";
4
4
  import type { EnvVarInput } from "@stigmer/sdk";
5
5
  import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
6
6
  import { usePersonalEnvironment } from "../environment/usePersonalEnvironment";
7
- import { diffEnvSpec } from "../environment/diffEnvSpec";
7
+ import { diffEnv } from "../environment/diffEnv";
8
8
  import { SYSTEM_ENV_VAR_KEYS } from "../environment/systemEnvVars";
9
9
  import type { EnvVarFormVariable } from "../environment/EnvVarForm";
10
+ import { useOAuthGrantStatus } from "./useOAuthGrantStatus";
10
11
 
11
12
  /**
12
13
  * Credential acquisition mode for an MCP server.
@@ -34,37 +35,50 @@ export interface UseMcpServerCredentialsReturn {
34
35
  */
35
36
  readonly oauthTargetEnvVar: string | null;
36
37
  /**
37
- * `true` when the OAuth-managed env var exists in the personal
38
- * environment. Always `false` when `authMode` is `"manual"`.
38
+ * `true` when the user has an active OAuth grant for this server.
39
+ * Derived from the `getOAuthGrantStatus` API, not from personal
40
+ * environment key presence. Always `false` when `authMode` is `"manual"`.
39
41
  */
40
42
  readonly isOAuthConnected: boolean;
43
+ /**
44
+ * When the OAuth access token expires (Unix timestamp seconds).
45
+ * `BigInt(0)` when no grant exists, `authMode` is `"manual"`, or the token
46
+ * does not expire. Useful for showing actual expiry in the UI.
47
+ */
48
+ readonly accessTokenExpiresAt: bigint;
41
49
  /**
42
50
  * Informational hint about expected token lifetime, or `null`.
43
51
  * Sourced from `spec.auth.token_lifetime_hint`.
44
52
  */
45
53
  readonly tokenLifetimeHint: string | null;
46
54
  /**
47
- * Variables required by the MCP server that are missing from the
48
- * user's personal environment. Empty when all variables are present
49
- * or the server has no `env_spec`.
55
+ * Required variables (non-optional) missing from the user's personal
56
+ * environment. Empty when all required variables are present, the
57
+ * server has no `env` declarations, or all declarations are optional.
50
58
  *
51
59
  * When `authMode` is `"oauth"`, the OAuth-managed `target_env_var`
52
60
  * 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.
61
+ * not via a manual form. Only additional non-OAuth required vars
62
+ * appear here.
63
+ *
64
+ * Optional env vars are never included — they are discoverable in
65
+ * the read-only EnvSection but do not block connect.
54
66
  *
55
67
  * Suitable as direct input to {@link EnvVarForm}.
56
68
  */
57
69
  readonly missingVariables: EnvVarFormVariable[];
58
70
  /**
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.
71
+ * `true` when all required (non-optional) credentials are available
72
+ * — both OAuth-managed and manual variables. For OAuth servers this
73
+ * means the OAuth grant is connected AND any additional required
74
+ * manual vars are present in the personal environment.
75
+ *
76
+ * Servers whose env vars are all optional are always ready.
63
77
  */
64
78
  readonly isReady: boolean;
65
- /** `true` while the personal environment is being fetched. */
79
+ /** `true` while the personal environment or grant status is being fetched. */
66
80
  readonly isLoading: boolean;
67
- /** Error from the personal environment fetch, or `null`. */
81
+ /** Error from the personal environment or grant status fetch, or `null`. */
68
82
  readonly error: Error | null;
69
83
  /**
70
84
  * Save the provided credentials to the user's personal environment.
@@ -81,7 +95,7 @@ export interface UseMcpServerCredentialsReturn {
81
95
 
82
96
  /**
83
97
  * Checks the user's personal environment against an MCP server's
84
- * `env_spec` and provides a mechanism to save missing credentials.
98
+ * `env` declarations and provides a mechanism to save missing credentials.
85
99
  *
86
100
  * Designed for the discovery flow on the MCP server detail page:
87
101
  * before triggering discovery, the UI needs to ensure all required
@@ -90,10 +104,12 @@ export interface UseMcpServerCredentialsReturn {
90
104
  * them.
91
105
  *
92
106
  * **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).
107
+ * composes {@link useOAuthGrantStatus} to determine whether the
108
+ * OAuth-managed variable (`target_env_var`) is connected via an
109
+ * active grant. The OAuth variable is excluded from `missingVariables`
110
+ * it is acquired via {@link useMcpServerOAuthConnect}, not a manual
111
+ * form. Additional non-OAuth vars still appear in `missingVariables`
112
+ * (mixed mode).
97
113
  *
98
114
  * Unlike {@link useMcpServerSetup} which manages multi-server setup
99
115
  * for session creation, this hook is scoped to a single server and
@@ -134,32 +150,43 @@ export function useMcpServerCredentials(
134
150
  const oauthTargetEnvVar = auth?.targetEnvVar || null;
135
151
  const tokenLifetimeHint = auth?.tokenLifetimeHint || null;
136
152
 
153
+ const grantStatus = useOAuthGrantStatus(
154
+ authMode === "oauth" ? (mcpServer?.metadata?.id ?? null) : null,
155
+ authMode === "oauth" ? org : null,
156
+ );
157
+
158
+ const isOAuthConnected = authMode === "oauth" && grantStatus.connected;
159
+
137
160
  const existingKeys = useMemo(
138
161
  () => new Set(Object.keys(personalEnv.environment?.spec?.data ?? {})),
139
162
  [personalEnv.environment],
140
163
  );
141
164
 
142
- const isOAuthConnected = authMode === "oauth"
143
- && oauthTargetEnvVar !== null
144
- && existingKeys.has(oauthTargetEnvVar);
145
-
146
165
  const allMissingVariables = useMemo(() => {
147
166
  if (!mcpServer) return [];
148
- const envSpecData = mcpServer.spec?.envSpec?.data;
149
- if (!envSpecData || Object.keys(envSpecData).length === 0) return [];
167
+ const envDeclarations = mcpServer.spec?.env;
168
+ if (!envDeclarations || Object.keys(envDeclarations).length === 0) return [];
150
169
 
151
- return diffEnvSpec(envSpecData, existingKeys).filter(
170
+ return diffEnv(envDeclarations, existingKeys).filter(
152
171
  (v) => !SYSTEM_ENV_VAR_KEYS.has(v.key),
153
172
  );
154
173
  }, [mcpServer, existingKeys]);
155
174
 
175
+ const requiredMissing = useMemo(
176
+ () => allMissingVariables.filter((v) => !v.optional),
177
+ [allMissingVariables],
178
+ );
179
+
156
180
  const missingVariables = useMemo(() => {
157
- if (!oauthTargetEnvVar) return allMissingVariables;
158
- return allMissingVariables.filter((v) => v.key !== oauthTargetEnvVar);
159
- }, [allMissingVariables, oauthTargetEnvVar]);
181
+ if (!oauthTargetEnvVar) return requiredMissing;
182
+ return requiredMissing.filter((v) => v.key !== oauthTargetEnvVar);
183
+ }, [requiredMissing, oauthTargetEnvVar]);
160
184
 
161
185
  const isReady =
162
- !personalEnv.isLoading && allMissingVariables.length === 0;
186
+ !personalEnv.isLoading &&
187
+ !grantStatus.isLoading &&
188
+ missingVariables.length === 0 &&
189
+ (authMode === "manual" || isOAuthConnected);
163
190
 
164
191
  const saveCredentials = useCallback(
165
192
  async (values: Record<string, EnvVarInput>): Promise<void> => {
@@ -169,17 +196,23 @@ export function useMcpServerCredentials(
169
196
  [personalEnv],
170
197
  );
171
198
 
199
+ const refetch = useCallback(() => {
200
+ personalEnv.refetch();
201
+ grantStatus.refetch();
202
+ }, [personalEnv, grantStatus]);
203
+
172
204
  return {
173
205
  authMode,
174
206
  oauthTargetEnvVar,
175
207
  isOAuthConnected,
208
+ accessTokenExpiresAt: grantStatus.accessTokenExpiresAt,
176
209
  tokenLifetimeHint,
177
210
  missingVariables,
178
211
  isReady,
179
- isLoading: personalEnv.isLoading,
180
- error: personalEnv.error,
212
+ isLoading: personalEnv.isLoading || grantStatus.isLoading,
213
+ error: personalEnv.error ?? grantStatus.error,
181
214
  saveCredentials,
182
215
  isSaving: personalEnv.isMutating,
183
- refetch: personalEnv.refetch,
216
+ refetch,
184
217
  };
185
218
  }
@@ -9,6 +9,7 @@ import {
9
9
  ConnectInputSchema,
10
10
  } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
11
11
  import { useStigmer } from "../hooks";
12
+ import { resolveSystemEnvVarValues } from "../environment/systemEnvVars";
12
13
  import { toError } from "../internal/toError";
13
14
 
14
15
  /**
@@ -53,14 +54,15 @@ export interface UseMcpServerOAuthConnectReturn {
53
54
  * blockers.
54
55
  *
55
56
  * @param mcpServerId - System-generated ID (metadata.id) of the MCP server.
57
+ * @param org - Organization context for token storage (caller's active org).
56
58
  * @returns The updated McpServer after tool discovery completes.
57
59
  */
58
- readonly startOAuth: (mcpServerId: string) => Promise<McpServer>;
60
+ readonly startOAuth: (mcpServerId: string, org: string) => Promise<McpServer>;
59
61
  /** `true` while any phase of the OAuth flow is in progress. */
60
62
  readonly isInProgress: boolean;
61
63
  /** Current phase of the OAuth flow. */
62
64
  readonly phase: OAuthConnectPhase;
63
- /** Error from the most recent failed attempt, or `null`. */
65
+ /** Error from the most recent unsuccessful attempt, or `null`. */
64
66
  readonly error: Error | null;
65
67
  /** Reset the hook to idle state, clearing any error. */
66
68
  readonly clearError: () => void;
@@ -91,7 +93,7 @@ const POPUP_CALLBACK_TIMEOUT_MS = 120_000;
91
93
  *
92
94
  * async function handleSignIn() {
93
95
  * try {
94
- * await oauth.startOAuth(mcpServer.metadata.id);
96
+ * await oauth.startOAuth(mcpServer.metadata.id, org);
95
97
  * refetch();
96
98
  * } catch {
97
99
  * // error is available via oauth.error
@@ -123,7 +125,7 @@ export function useMcpServerOAuthConnect(): UseMcpServerOAuthConnectReturn {
123
125
  }, []);
124
126
 
125
127
  const startOAuth = useCallback(
126
- async (mcpServerId: string): Promise<McpServer> => {
128
+ async (mcpServerId: string, org: string): Promise<McpServer> => {
127
129
  setPhase("initiating");
128
130
  setError(null);
129
131
 
@@ -151,7 +153,7 @@ export function useMcpServerOAuthConnect(): UseMcpServerOAuthConnectReturn {
151
153
 
152
154
  try {
153
155
  const initOutput = await stigmer.mcpServer.initiateOAuthConnect(
154
- create(InitiateOAuthConnectInputSchema, { mcpServerId }),
156
+ create(InitiateOAuthConnectInputSchema, { mcpServerId, org }),
155
157
  );
156
158
 
157
159
  popup.location.href = initOutput.authorizationUrl;
@@ -177,8 +179,20 @@ export function useMcpServerOAuthConnect(): UseMcpServerOAuthConnectReturn {
177
179
 
178
180
  setPhase("connecting");
179
181
 
182
+ const systemEnv = await resolveSystemEnvVarValues(stigmer);
183
+ const runtimeEnvMap: Record<string, { value: string; isSecret: boolean }> = {};
184
+ for (const [key, envInput] of Object.entries(systemEnv)) {
185
+ runtimeEnvMap[key] = {
186
+ value: envInput.value,
187
+ isSecret: envInput.isSecret ?? false,
188
+ };
189
+ }
190
+
180
191
  const server = await stigmer.mcpServer.connect(
181
- create(ConnectInputSchema, { mcpServerId }),
192
+ create(ConnectInputSchema, {
193
+ mcpServerId,
194
+ runtimeEnv: runtimeEnvMap,
195
+ }),
182
196
  );
183
197
 
184
198
  setPhase("done");
@@ -2,12 +2,14 @@
2
2
 
3
3
  import { useCallback, useEffect, useMemo, useReducer, useRef } from "react";
4
4
  import type { EnvVarInput, McpServerUsageInput, ResourceRef } from "@stigmer/sdk";
5
+ import { create } from "@bufbuild/protobuf";
5
6
  import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
7
+ import { GetOAuthGrantStatusInputSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
6
8
  import type { DiscoveredTool } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/status_pb";
7
9
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
8
10
  import { useStigmer } from "../hooks";
9
11
  import { usePersonalEnvironment } from "../environment/usePersonalEnvironment";
10
- import { diffEnvSpec } from "../environment/diffEnvSpec";
12
+ import { diffEnv } from "../environment/diffEnv";
11
13
  import { toError } from "../internal/toError";
12
14
  import {
13
15
  mcpServerSetupReducer,
@@ -57,8 +59,8 @@ export interface UseMcpServerSetupReturn {
57
59
  /**
58
60
  * Add an MCP server to the setup flow.
59
61
  *
60
- * Fetches the full server resource, checks `env_spec` against the
61
- * personal environment, and resolves the entry to either `ready`
62
+ * Fetches the full server resource, checks `env` declarations against
63
+ * the personal environment, and resolves the entry to either `ready`
62
64
  * (no credentials needed or all present) or `needsSetup` (missing
63
65
  * variables). Also extracts discovered tools and approval policies
64
66
  * for the tool selector.
@@ -171,8 +173,8 @@ function computeDefaultEnabledTools(
171
173
  * MCP servers selected in the {@link McpServerPicker}.
172
174
  *
173
175
  * When a user toggles an MCP server ON, this hook fetches the server's
174
- * full resource, checks its `env_spec` against the personal environment
175
- * (via {@link diffEnvSpec}), and determines whether credentials are
176
+ * full resource, checks its `env` declarations against the personal
177
+ * environment (via {@link diffEnv}), and determines whether credentials are
176
178
  * needed. It also extracts discovered tools and approval policies for
177
179
  * the tool selector UI.
178
180
  *
@@ -198,7 +200,7 @@ function computeDefaultEnabledTools(
198
200
  * @param org - Organization slug. Pass `null` to disable.
199
201
  * @param poolKeys - Optional set of env-var keys already available
200
202
  * from the session env pool (manual secrets, one-time env vars from
201
- * other components). When provided, servers whose `env_spec` keys
203
+ * other components). When provided, servers whose `env` keys
202
204
  * are fully covered by `poolKeys` + personal env auto-resolve to
203
205
  * `ready` without prompting. Reactive — when `poolKeys` changes,
204
206
  * `needsSetup` entries are re-evaluated.
@@ -264,9 +266,9 @@ export function useMcpServerSetup(
264
266
  const discoveredTools =
265
267
  mcpServer.status?.discoveredCapabilities?.tools ?? [];
266
268
  const toolApprovals = mcpServer.spec?.pinnedToolApprovals ?? [];
267
- const envSpecData = mcpServer.spec?.envSpec?.data;
269
+ const envDeclarations = mcpServer.spec?.env;
268
270
 
269
- if (!envSpecData || Object.keys(envSpecData).length === 0) {
271
+ if (!envDeclarations || Object.keys(envDeclarations).length === 0) {
270
272
  dispatch({
271
273
  type: "RESOLVE_READY",
272
274
  key,
@@ -284,9 +286,29 @@ export function useMcpServerSetup(
284
286
  const existingKeys = new Set(
285
287
  Object.keys(personalEnv.environment?.spec?.data ?? {}),
286
288
  );
287
- const missingVariables = diffEnvSpec(envSpecData, existingKeys, poolKeys);
288
289
 
289
- if (missingVariables.length === 0) {
290
+ const auth = mcpServer.spec?.auth;
291
+ if (auth?.targetEnvVar && mcpServer.metadata?.id) {
292
+ try {
293
+ const grantStatus = await stigmer.mcpServer.getOAuthGrantStatus(
294
+ create(GetOAuthGrantStatusInputSchema, {
295
+ resourceId: mcpServer.metadata.id,
296
+ org,
297
+ }),
298
+ );
299
+ if (grantStatus.connected) {
300
+ existingKeys.add(auth.targetEnvVar);
301
+ }
302
+ } catch {
303
+ // Non-fatal: if grant status lookup fails, the OAuth var stays
304
+ // in missingVariables and the UI shows the sign-in button.
305
+ }
306
+ }
307
+
308
+ const allMissing = diffEnv(envDeclarations, existingKeys, poolKeys);
309
+ const requiredMissing = allMissing.filter((v) => !v.optional);
310
+
311
+ if (requiredMissing.length === 0) {
290
312
  dispatch({
291
313
  type: "RESOLVE_READY",
292
314
  key,
@@ -305,7 +327,7 @@ export function useMcpServerSetup(
305
327
  type: "RESOLVE_NEEDS_SETUP",
306
328
  key,
307
329
  mcpServer,
308
- missingVariables,
330
+ missingVariables: requiredMissing,
309
331
  discoveredTools,
310
332
  toolApprovals,
311
333
  });
@@ -420,15 +442,16 @@ export function useMcpServerSetup(
420
442
  for (const [key, entry] of Object.entries(entriesRef.current)) {
421
443
  if (entry.status !== "needsSetup") continue;
422
444
 
423
- const envSpecData = entry.mcpServer.spec?.envSpec?.data;
424
- if (!envSpecData) continue;
445
+ const envDeclarations = entry.mcpServer.spec?.env;
446
+ if (!envDeclarations) continue;
425
447
 
426
- const missingVariables = diffEnvSpec(envSpecData, personalKeys, poolKeys);
448
+ const allMissing = diffEnv(envDeclarations, personalKeys, poolKeys);
449
+ const requiredMissing = allMissing.filter((v) => !v.optional);
427
450
  const enabledTools = computeDefaultEnabledTools(
428
451
  entry.mcpServer,
429
452
  entry.discoveredTools,
430
453
  );
431
- dispatch({ type: "POOL_RESOLVE", key, missingVariables, enabledTools });
454
+ dispatch({ type: "POOL_RESOLVE", key, missingVariables: requiredMissing, enabledTools });
432
455
  }
433
456
  }, [poolKeys, personalEnv.environment]);
434
457
 
@@ -0,0 +1,125 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useState } from "react";
4
+ import { create } from "@bufbuild/protobuf";
5
+ import { GetOAuthGrantStatusInputSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
6
+ import { useStigmer } from "../hooks";
7
+ import { toError } from "../internal/toError";
8
+
9
+ /** Return value of {@link useOAuthGrantStatus}. */
10
+ export interface UseOAuthGrantStatusReturn {
11
+ /** Whether the user has an active OAuth grant for this resource + org. */
12
+ readonly connected: boolean;
13
+ /**
14
+ * When the access token expires (Unix timestamp seconds).
15
+ * `BigInt(0)` when no grant exists or the token does not expire.
16
+ */
17
+ readonly accessTokenExpiresAt: bigint;
18
+ /** The env var name managed by OAuth, or empty string when no grant exists. */
19
+ readonly targetEnvVar: string;
20
+ /** Auth method used (`"mcp_oauth"` or `"vendor_oauth"`), or empty string. */
21
+ readonly authMethod: string;
22
+ /** `true` while the grant status is being fetched. */
23
+ readonly isLoading: boolean;
24
+ /** Error from the last failed request, or `null` when healthy. */
25
+ readonly error: Error | null;
26
+ /** Discard cached data and re-fetch the grant status. */
27
+ readonly refetch: () => void;
28
+ }
29
+
30
+ const BIGINT_ZERO = BigInt(0);
31
+
32
+ const IDLE: UseOAuthGrantStatusReturn = {
33
+ connected: false,
34
+ accessTokenExpiresAt: BIGINT_ZERO,
35
+ targetEnvVar: "",
36
+ authMethod: "",
37
+ isLoading: false,
38
+ error: null,
39
+ refetch: () => {},
40
+ };
41
+
42
+ /**
43
+ * Data hook that fetches the OAuth grant status for a single MCP server.
44
+ *
45
+ * Wraps `stigmer.mcpServer.getOAuthGrantStatus()` with loading, error,
46
+ * and idle state management. When `resourceId` or `org` changes, the
47
+ * previous in-flight request is discarded and a fresh fetch begins.
48
+ *
49
+ * Pass `null` for either parameter to skip fetching (stable no-op).
50
+ * This is useful when the MCP server resource has not loaded yet.
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * const grantStatus = useOAuthGrantStatus(mcpServer?.metadata?.id ?? null, org);
55
+ *
56
+ * if (grantStatus.isLoading) return <Spinner />;
57
+ * if (grantStatus.connected) return <span>Connected via OAuth</span>;
58
+ * return <button onClick={startOAuth}>Sign in</button>;
59
+ * ```
60
+ */
61
+ export function useOAuthGrantStatus(
62
+ resourceId: string | null,
63
+ org: string | null,
64
+ ): UseOAuthGrantStatusReturn {
65
+ const stigmer = useStigmer();
66
+ const [connected, setConnected] = useState(false);
67
+ const [accessTokenExpiresAt, setAccessTokenExpiresAt] = useState(BIGINT_ZERO);
68
+ const [targetEnvVar, setTargetEnvVar] = useState("");
69
+ const [authMethod, setAuthMethod] = useState("");
70
+ const [isLoading, setIsLoading] = useState(false);
71
+ const [error, setError] = useState<Error | null>(null);
72
+ const [fetchKey, setFetchKey] = useState(0);
73
+
74
+ const refetch = useCallback(() => setFetchKey((k) => k + 1), []);
75
+
76
+ useEffect(() => {
77
+ if (!resourceId || !org) {
78
+ setConnected(false);
79
+ setAccessTokenExpiresAt(BIGINT_ZERO);
80
+ setTargetEnvVar("");
81
+ setAuthMethod("");
82
+ setIsLoading(false);
83
+ setError(null);
84
+ return;
85
+ }
86
+
87
+ const cancelled = { current: false };
88
+ setIsLoading(true);
89
+ setError(null);
90
+
91
+ stigmer.mcpServer
92
+ .getOAuthGrantStatus(create(GetOAuthGrantStatusInputSchema, { resourceId, org }))
93
+ .then(
94
+ (result) => {
95
+ if (cancelled.current) return;
96
+ setConnected(result.connected);
97
+ setAccessTokenExpiresAt(result.accessTokenExpiresAt);
98
+ setTargetEnvVar(result.targetEnvVar);
99
+ setAuthMethod(result.authMethod);
100
+ setIsLoading(false);
101
+ },
102
+ (err) => {
103
+ if (cancelled.current) return;
104
+ setError(toError(err));
105
+ setIsLoading(false);
106
+ },
107
+ );
108
+
109
+ return () => {
110
+ cancelled.current = true;
111
+ };
112
+ }, [resourceId, org, stigmer, fetchKey]);
113
+
114
+ if (!resourceId || !org) return { ...IDLE, refetch };
115
+
116
+ return {
117
+ connected,
118
+ accessTokenExpiresAt,
119
+ targetEnvVar,
120
+ authMethod,
121
+ isLoading,
122
+ error,
123
+ refetch,
124
+ };
125
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"diffEnvSpec.d.ts","sourceRoot":"","sources":["../../src/environment/diffEnvSpec.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,WAAW,CACzB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,EACxE,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,EACzB,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GACrB,kBAAkB,EAAE,CAetB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"diffEnvSpec.js","sourceRoot":"","sources":["../../src/environment/diffEnvSpec.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,WAAW,CACzB,WAAwE,EACxE,YAAyB,EACzB,QAAsB;IAEtB,MAAM,OAAO,GAAyB,EAAE,CAAC;IAEzC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;QACvD,IAAI,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAS;QACpC,IAAI,QAAQ,EAAE,GAAG,CAAC,GAAG,CAAC;YAAE,SAAS;QAEjC,OAAO,CAAC,IAAI,CAAC;YACX,GAAG;YACH,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,GAAG,CAAC,KAAK,CAAC,WAAW,IAAI,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC;SAC7D,CAAC,CAAC;IACL,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}