@stigmer/react 0.0.79 → 0.0.81

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 (100) 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 +2 -2
  25. package/environment/index.d.ts.map +1 -1
  26. package/environment/index.js +2 -2
  27. package/environment/index.js.map +1 -1
  28. package/environment/systemEnvVars.d.ts +16 -0
  29. package/environment/systemEnvVars.d.ts.map +1 -1
  30. package/environment/systemEnvVars.js +32 -0
  31. package/environment/systemEnvVars.js.map +1 -1
  32. package/execution/SessionVariablesInput.d.ts +1 -1
  33. package/index.d.ts +4 -4
  34. package/index.d.ts.map +1 -1
  35. package/index.js +4 -4
  36. package/index.js.map +1 -1
  37. package/library/parse-resource-yaml.d.ts +1 -1
  38. package/library/parse-resource-yaml.d.ts.map +1 -1
  39. package/library/parse-resource-yaml.js +26 -16
  40. package/library/parse-resource-yaml.js.map +1 -1
  41. package/library/serialize-resource-yaml.js +17 -21
  42. package/library/serialize-resource-yaml.js.map +1 -1
  43. package/mcp-server/McpServerDetailView.d.ts +8 -1
  44. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  45. package/mcp-server/McpServerDetailView.js +34 -9
  46. package/mcp-server/McpServerDetailView.js.map +1 -1
  47. package/mcp-server/McpServerPicker.d.ts +8 -1
  48. package/mcp-server/McpServerPicker.d.ts.map +1 -1
  49. package/mcp-server/McpServerPicker.js +2 -2
  50. package/mcp-server/McpServerPicker.js.map +1 -1
  51. package/mcp-server/index.d.ts +2 -0
  52. package/mcp-server/index.d.ts.map +1 -1
  53. package/mcp-server/index.js +1 -0
  54. package/mcp-server/index.js.map +1 -1
  55. package/mcp-server/mcpServerSetupReducer.d.ts +4 -4
  56. package/mcp-server/useMcpServerConnect.d.ts +22 -12
  57. package/mcp-server/useMcpServerConnect.d.ts.map +1 -1
  58. package/mcp-server/useMcpServerConnect.js +17 -10
  59. package/mcp-server/useMcpServerConnect.js.map +1 -1
  60. package/mcp-server/useMcpServerCredentials.d.ts +32 -17
  61. package/mcp-server/useMcpServerCredentials.d.ts.map +1 -1
  62. package/mcp-server/useMcpServerCredentials.js +30 -19
  63. package/mcp-server/useMcpServerCredentials.js.map +1 -1
  64. package/mcp-server/useMcpServerOAuthConnect.d.ts +3 -1
  65. package/mcp-server/useMcpServerOAuthConnect.d.ts.map +1 -1
  66. package/mcp-server/useMcpServerOAuthConnect.js +12 -6
  67. package/mcp-server/useMcpServerOAuthConnect.js.map +1 -1
  68. package/mcp-server/useMcpServerSetup.d.ts +5 -5
  69. package/mcp-server/useMcpServerSetup.d.ts.map +1 -1
  70. package/mcp-server/useMcpServerSetup.js +33 -13
  71. package/mcp-server/useMcpServerSetup.js.map +1 -1
  72. package/mcp-server/useOAuthGrantStatus.d.ts +41 -0
  73. package/mcp-server/useOAuthGrantStatus.d.ts.map +1 -0
  74. package/mcp-server/useOAuthGrantStatus.js +91 -0
  75. package/mcp-server/useOAuthGrantStatus.js.map +1 -0
  76. package/package.json +4 -4
  77. package/src/agent/AgentDetailView.tsx +5 -5
  78. package/src/agent/agentSetupReducer.ts +3 -3
  79. package/src/agent/index.ts +1 -1
  80. package/src/agent/useAgentSetup.ts +11 -11
  81. package/src/environment/EnvVarForm.tsx +9 -3
  82. package/src/environment/EnvironmentListPanel.tsx +27 -9
  83. package/src/environment/{diffEnvSpec.ts → diffEnv.ts} +10 -9
  84. package/src/environment/index.ts +2 -1
  85. package/src/environment/systemEnvVars.ts +39 -0
  86. package/src/execution/SessionVariablesInput.tsx +1 -1
  87. package/src/index.ts +5 -2
  88. package/src/library/parse-resource-yaml.ts +27 -18
  89. package/src/library/serialize-resource-yaml.ts +20 -27
  90. package/src/mcp-server/McpServerDetailView.tsx +46 -10
  91. package/src/mcp-server/McpServerPicker.tsx +9 -1
  92. package/src/mcp-server/index.ts +3 -0
  93. package/src/mcp-server/mcpServerSetupReducer.ts +4 -4
  94. package/src/mcp-server/useMcpServerConnect.ts +33 -14
  95. package/src/mcp-server/useMcpServerCredentials.ts +65 -32
  96. package/src/mcp-server/useMcpServerOAuthConnect.ts +17 -10
  97. package/src/mcp-server/useMcpServerSetup.ts +38 -15
  98. package/src/mcp-server/useOAuthGrantStatus.ts +125 -0
  99. package/environment/diffEnvSpec.d.ts.map +0 -1
  100. package/environment/diffEnvSpec.js.map +0 -1
@@ -10,7 +10,7 @@ import type {
10
10
  } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/status_pb";
11
11
  import type { ToolApprovalPolicy, McpServerSpec } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
12
12
  import { ValidationState } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/status_pb";
13
- import type { EnvironmentValue } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/spec_pb";
13
+ import type { EnvVarDeclaration } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/spec_pb";
14
14
  import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
15
15
  import { useMcpServer } from "./useMcpServer";
16
16
  import { useMcpServerConnect } from "./useMcpServerConnect";
@@ -73,6 +73,13 @@ export interface McpServerDetailViewProps {
73
73
  readonly credentialPoolValues?: (
74
74
  key: string,
75
75
  ) => import("@stigmer/sdk").EnvVarInput | undefined;
76
+ /**
77
+ * The authenticated user's active organization slug.
78
+ * Used for OAuth token storage — tokens are stored in the user's personal
79
+ * environment within this org, not the MCP server's org.
80
+ * When omitted, falls back to the `org` prop (MCP server's org).
81
+ */
82
+ readonly activeOrg?: string;
76
83
  /** Additional CSS classes for the root container. */
77
84
  readonly className?: string;
78
85
  }
@@ -110,6 +117,7 @@ export function McpServerDetailView({
110
117
  defaultCapabilityTab = "tools",
111
118
  defaultShowCredentialForm = false,
112
119
  credentialPoolValues,
120
+ activeOrg,
113
121
  className,
114
122
  }: McpServerDetailViewProps) {
115
123
  const { mcpServer, isLoading, error, refetch } = useMcpServer(org, slug);
@@ -132,8 +140,9 @@ export function McpServerDetailView({
132
140
  const handleOAuthSignIn = useCallback(async () => {
133
141
  if (!mcpServer?.metadata?.id) return;
134
142
 
143
+ const envKeys = Object.keys(mcpServer.spec?.env ?? {});
135
144
  try {
136
- await oauth.startOAuth(mcpServer.metadata.id, org);
145
+ await oauth.startOAuth(mcpServer.metadata.id, activeOrg ?? org, envKeys);
137
146
  credentials.refetch();
138
147
  refetch();
139
148
  } catch {
@@ -154,8 +163,9 @@ export function McpServerDetailView({
154
163
  return;
155
164
  }
156
165
 
166
+ const envKeys = Object.keys(mcpServer.spec?.env ?? {});
157
167
  try {
158
- await connection.connect(mcpServer.metadata.id);
168
+ await connection.connect(mcpServer.metadata.id, activeOrg ?? org, undefined, envKeys);
159
169
  refetch();
160
170
  } catch {
161
171
  // error state is managed by the hook
@@ -176,10 +186,12 @@ export function McpServerDetailView({
176
186
  setShowCredentialForm(false);
177
187
 
178
188
  if (mcpServer?.metadata?.id) {
189
+ const envKeys = Object.keys(mcpServer.spec?.env ?? {});
190
+ const connectOrg = activeOrg ?? org;
179
191
  if (options.saveForFuture) {
180
- await connection.connect(mcpServer.metadata.id);
192
+ await connection.connect(mcpServer.metadata.id, connectOrg, undefined, envKeys);
181
193
  } else {
182
- await connection.connect(mcpServer.metadata.id, values);
194
+ await connection.connect(mcpServer.metadata.id, connectOrg, values, envKeys);
183
195
  }
184
196
  refetch();
185
197
  }
@@ -258,9 +270,9 @@ export function McpServerDetailView({
258
270
  <ServerConfigSection serverType={spec.serverType} />
259
271
  )}
260
272
 
261
- {spec?.envSpec && Object.keys(spec.envSpec.data).length > 0 && (
262
- <EnvSpecSection
263
- data={spec.envSpec.data}
273
+ {spec?.env && Object.keys(spec.env).length > 0 && (
274
+ <EnvSection
275
+ data={spec.env}
264
276
  oauthTargetEnvVar={credentials.oauthTargetEnvVar}
265
277
  />
266
278
  )}
@@ -278,6 +290,7 @@ export function McpServerDetailView({
278
290
  oauthPhase={oauth.phase}
279
291
  authMode={credentials.authMode}
280
292
  isOAuthConnected={credentials.isOAuthConnected}
293
+ accessTokenExpiresAt={credentials.accessTokenExpiresAt}
281
294
  tokenLifetimeHint={credentials.tokenLifetimeHint}
282
295
  />
283
296
 
@@ -346,6 +359,7 @@ function ConnectBar({
346
359
  oauthPhase,
347
360
  authMode,
348
361
  isOAuthConnected,
362
+ accessTokenExpiresAt,
349
363
  tokenLifetimeHint,
350
364
  }: {
351
365
  readonly isConnecting: boolean;
@@ -359,6 +373,7 @@ function ConnectBar({
359
373
  readonly oauthPhase: OAuthConnectPhase;
360
374
  readonly authMode: "manual" | "oauth";
361
375
  readonly isOAuthConnected: boolean;
376
+ readonly accessTokenExpiresAt: bigint;
362
377
  readonly tokenLifetimeHint: string | null;
363
378
  }) {
364
379
  const isOAuthBusy =
@@ -384,6 +399,8 @@ function ConnectBar({
384
399
 
385
400
  const statusText = (() => {
386
401
  if (authMode === "oauth" && isOAuthConnected) {
402
+ const expiryLabel = formatTokenExpiry(accessTokenExpiresAt);
403
+ if (expiryLabel) return `Tokens refresh automatically \u00B7 ${expiryLabel}`;
387
404
  const hint = tokenLifetimeHint && tokenLifetimeHint !== "never"
388
405
  ? ` \u00B7 Session lasts ~${tokenLifetimeHint}`
389
406
  : "";
@@ -480,6 +497,20 @@ function formatConnectionSummary(toolCount: number, policyCount: number): string
480
497
  return `${toolLabel}, ${policyLabel}`;
481
498
  }
482
499
 
500
+ function formatTokenExpiry(expiresAtSeconds: bigint): string | null {
501
+ if (expiresAtSeconds === BigInt(0)) return null;
502
+ const nowSeconds = BigInt(Math.floor(Date.now() / 1000));
503
+ const remainingSeconds = expiresAtSeconds - nowSeconds;
504
+ if (remainingSeconds <= BigInt(0)) return "Token expired";
505
+ const minutes = Number(remainingSeconds / BigInt(60));
506
+ if (minutes < 1) return "Expires in <1 min";
507
+ if (minutes < 60) return `Expires in ${minutes} min`;
508
+ const hours = Math.floor(minutes / 60);
509
+ if (hours < 24) return `Expires in ${hours}h ${minutes % 60}m`;
510
+ const days = Math.floor(hours / 24);
511
+ return `Expires in ${days}d`;
512
+ }
513
+
483
514
  // ---------------------------------------------------------------------------
484
515
  // Internal section components
485
516
  // ---------------------------------------------------------------------------
@@ -760,11 +791,11 @@ function ResourceTemplatesList({
760
791
  );
761
792
  }
762
793
 
763
- function EnvSpecSection({
794
+ function EnvSection({
764
795
  data,
765
796
  oauthTargetEnvVar,
766
797
  }: {
767
- readonly data: { [key: string]: EnvironmentValue };
798
+ readonly data: { [key: string]: EnvVarDeclaration };
768
799
  readonly oauthTargetEnvVar: string | null;
769
800
  }) {
770
801
  const entries = Object.entries(data).sort(([a], [b]) =>
@@ -789,6 +820,11 @@ function EnvSpecSection({
789
820
  oauth
790
821
  </span>
791
822
  )}
823
+ {env.optional && (
824
+ <span className="shrink-0 rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground/70">
825
+ optional
826
+ </span>
827
+ )}
792
828
  {env.description && (
793
829
  <span className="text-xs text-muted-foreground">
794
830
  {env.description}
@@ -155,6 +155,13 @@ export interface McpServerPickerProps {
155
155
  * pre-populated.
156
156
  */
157
157
  readonly poolValues?: (key: string) => EnvVarInput | undefined;
158
+ /**
159
+ * The authenticated user's active organization slug.
160
+ * Used for OAuth token storage — tokens are stored in the user's personal
161
+ * environment within this org, not the MCP server's org.
162
+ * When omitted, falls back to the `org` prop.
163
+ */
164
+ readonly activeOrg?: string;
158
165
  }
159
166
 
160
167
  // ---------------------------------------------------------------------------
@@ -250,6 +257,7 @@ export function McpServerPicker({
250
257
  setup,
251
258
  initialServerKey,
252
259
  poolValues,
260
+ activeOrg,
253
261
  }: McpServerPickerProps) {
254
262
  const instanceId = useId();
255
263
  const listId = `${instanceId}-list`;
@@ -408,7 +416,7 @@ export function McpServerPicker({
408
416
  onSignIn: async () => {
409
417
  if (!entry.mcpServer.metadata?.id) return;
410
418
  try {
411
- await oauth.startOAuth(entry.mcpServer.metadata.id, org);
419
+ await oauth.startOAuth(entry.mcpServer.metadata.id, activeOrg ?? org);
412
420
  setup.onServerAdded(ref);
413
421
  } catch {
414
422
  // error state managed by oauth hook
@@ -19,6 +19,9 @@ export type {
19
19
  export { useMcpServer } from "./useMcpServer";
20
20
  export type { UseMcpServerReturn } from "./useMcpServer";
21
21
 
22
+ export { useOAuthGrantStatus } from "./useOAuthGrantStatus";
23
+ export type { UseOAuthGrantStatusReturn } from "./useOAuthGrantStatus";
24
+
22
25
  export { McpServerPicker } from "./McpServerPicker";
23
26
  export type {
24
27
  McpServerPickerProps,
@@ -35,15 +35,15 @@ export function toServerKey(ref: ResourceRef): string {
35
35
  *
36
36
  * Phases:
37
37
  * - `"loading"` — Fetching the full `McpServer` resource (spec + status).
38
- * - `"needsSetup"` — The server has an `env_spec` with variables missing
39
- * from the user's personal environment. The UI should present a
40
- * credential collection form.
38
+ * - `"needsSetup"` — The server has `env` declarations with variables
39
+ * missing from the user's personal environment. The UI should present
40
+ * a credential collection form.
41
41
  * - `"submitting"` — Environment variables are being persisted (save path)
42
42
  * or collected (one-time path). The UI should show a loading indicator
43
43
  * on the submit button.
44
44
  * - `"ready"` — The server is fully configured and ready for session
45
45
  * creation. Carries the effective `enabledTools` list. Covers both
46
- * the "no env_spec needed" and "env vars resolved" cases.
46
+ * the "no env needed" and "env vars resolved" cases.
47
47
  */
48
48
  export type McpServerSetupPhase =
49
49
  | {
@@ -6,7 +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
+ import { resolveDeclaredSystemEnvVars } from "../environment/systemEnvVars";
10
10
  import { toError } from "../internal/toError";
11
11
 
12
12
  /** Return value of {@link useMcpServerConnect}. */
@@ -20,20 +20,30 @@ export interface UseMcpServerConnectReturn {
20
20
  * (typically 5-15 seconds, ~30s timeout).
21
21
  *
22
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.
23
+ * `STIGMER_API_KEY`) are injected only when the MCP server
24
+ * declares them in its `spec.env`. Pass `declaredEnvKeys` so the
25
+ * hook can determine which system vars the server actually needs.
26
+ * When omitted, no system vars are injected.
27
+ *
28
+ * When `runtimeEnv` is provided, those values are merged on top
29
+ * (caller values win). The backend merges the result on top of
30
+ * the user's personal environment so saved credentials (e.g.,
31
+ * OAuth tokens) are still resolved.
28
32
  *
29
33
  * @param mcpServerId - System-generated ID of the MCP server (metadata.id).
34
+ * @param org - The caller's active organization slug. Required for
35
+ * OAuth grant lookup and personal environment resolution.
30
36
  * @param runtimeEnv - Optional additional environment variables.
37
+ * @param declaredEnvKeys - Keys from the server's `spec.env` declaration.
38
+ * System vars are only injected when declared here.
31
39
  * @returns The updated McpServer with populated status.discovered_capabilities
32
40
  * and status.tool_approvals.
33
41
  */
34
42
  readonly connect: (
35
43
  mcpServerId: string,
44
+ org: string,
36
45
  runtimeEnv?: Record<string, EnvVarInput>,
46
+ declaredEnvKeys?: readonly string[],
37
47
  ) => Promise<McpServer>;
38
48
  /** `true` while the connect RPC is in flight. */
39
49
  readonly isConnecting: boolean;
@@ -52,10 +62,10 @@ export interface UseMcpServerConnectReturn {
52
62
  * approval policy via a structured-output LLM call.
53
63
  *
54
64
  * 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.
65
+ * `STIGMER_API_KEY`) are injected only when the target MCP server
66
+ * declares them in `spec.env`. Pass the server's declared env keys
67
+ * via `declaredEnvKeys` so the hook knows which system vars to
68
+ * include. When not provided, no system vars are injected.
59
69
  *
60
70
  * Additional one-time credentials can be passed via `runtimeEnv`
61
71
  * and will override both system vars and personal env values.
@@ -67,13 +77,15 @@ export interface UseMcpServerConnectReturn {
67
77
  *
68
78
  * // Saved credentials: already in personal environment
69
79
  * async function handleConnectSaved() {
70
- * await connect(mcpServer.metadata.id);
80
+ * const envKeys = Object.keys(mcpServer.spec?.env ?? {});
81
+ * await connect(mcpServer.metadata.id, undefined, envKeys);
71
82
  * refetch();
72
83
  * }
73
84
  *
74
85
  * // One-time use: pass credentials directly
75
86
  * async function handleConnectTemporary(values: Record<string, EnvVarInput>) {
76
- * await connect(mcpServer.metadata.id, values);
87
+ * const envKeys = Object.keys(mcpServer.spec?.env ?? {});
88
+ * await connect(mcpServer.metadata.id, values, envKeys);
77
89
  * refetch();
78
90
  * }
79
91
  * ```
@@ -88,13 +100,17 @@ export function useMcpServerConnect(): UseMcpServerConnectReturn {
88
100
  const connect = useCallback(
89
101
  async (
90
102
  mcpServerId: string,
103
+ org: string,
91
104
  runtimeEnv?: Record<string, EnvVarInput>,
105
+ declaredEnvKeys?: readonly string[],
92
106
  ): Promise<McpServer> => {
93
107
  setIsConnecting(true);
94
108
  setError(null);
95
109
 
96
110
  try {
97
- const systemEnv = await resolveSystemEnvVarValues(stigmer);
111
+ const systemEnv = declaredEnvKeys
112
+ ? await resolveDeclaredSystemEnvVars(stigmer, declaredEnvKeys)
113
+ : {};
98
114
  const mergedEnv = { ...systemEnv, ...(runtimeEnv ?? {}) };
99
115
 
100
116
  const runtimeEnvMap: Record<string, { value: string; isSecret: boolean }> = {};
@@ -107,7 +123,10 @@ export function useMcpServerConnect(): UseMcpServerConnectReturn {
107
123
 
108
124
  const input = create(ConnectInputSchema, {
109
125
  mcpServerId,
110
- runtimeEnv: runtimeEnvMap,
126
+ org,
127
+ ...(Object.keys(runtimeEnvMap).length > 0
128
+ ? { runtimeEnv: runtimeEnvMap }
129
+ : {}),
111
130
  });
112
131
 
113
132
  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,7 +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
+ import { resolveDeclaredSystemEnvVars } from "../environment/systemEnvVars";
13
13
  import { toError } from "../internal/toError";
14
14
 
15
15
  /**
@@ -55,9 +55,11 @@ export interface UseMcpServerOAuthConnectReturn {
55
55
  *
56
56
  * @param mcpServerId - System-generated ID (metadata.id) of the MCP server.
57
57
  * @param org - Organization context for token storage (caller's active org).
58
+ * @param declaredEnvKeys - Keys from the server's `spec.env` declaration.
59
+ * System vars are only injected when declared here.
58
60
  * @returns The updated McpServer after tool discovery completes.
59
61
  */
60
- readonly startOAuth: (mcpServerId: string, org: string) => Promise<McpServer>;
62
+ readonly startOAuth: (mcpServerId: string, org: string, declaredEnvKeys?: readonly string[]) => Promise<McpServer>;
61
63
  /** `true` while any phase of the OAuth flow is in progress. */
62
64
  readonly isInProgress: boolean;
63
65
  /** Current phase of the OAuth flow. */
@@ -125,7 +127,7 @@ export function useMcpServerOAuthConnect(): UseMcpServerOAuthConnectReturn {
125
127
  }, []);
126
128
 
127
129
  const startOAuth = useCallback(
128
- async (mcpServerId: string, org: string): Promise<McpServer> => {
130
+ async (mcpServerId: string, org: string, declaredEnvKeys?: readonly string[]): Promise<McpServer> => {
129
131
  setPhase("initiating");
130
132
  setError(null);
131
133
 
@@ -179,7 +181,9 @@ export function useMcpServerOAuthConnect(): UseMcpServerOAuthConnectReturn {
179
181
 
180
182
  setPhase("connecting");
181
183
 
182
- const systemEnv = await resolveSystemEnvVarValues(stigmer);
184
+ const systemEnv = declaredEnvKeys
185
+ ? await resolveDeclaredSystemEnvVars(stigmer, declaredEnvKeys)
186
+ : {};
183
187
  const runtimeEnvMap: Record<string, { value: string; isSecret: boolean }> = {};
184
188
  for (const [key, envInput] of Object.entries(systemEnv)) {
185
189
  runtimeEnvMap[key] = {
@@ -188,12 +192,15 @@ export function useMcpServerOAuthConnect(): UseMcpServerOAuthConnectReturn {
188
192
  };
189
193
  }
190
194
 
191
- const server = await stigmer.mcpServer.connect(
192
- create(ConnectInputSchema, {
193
- mcpServerId,
194
- runtimeEnv: runtimeEnvMap,
195
- }),
196
- );
195
+ const input = create(ConnectInputSchema, {
196
+ mcpServerId,
197
+ org,
198
+ ...(Object.keys(runtimeEnvMap).length > 0
199
+ ? { runtimeEnv: runtimeEnvMap }
200
+ : {}),
201
+ });
202
+
203
+ const server = await stigmer.mcpServer.connect(input);
197
204
 
198
205
  setPhase("done");
199
206
  return server;