@stigmer/react 0.0.79 → 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 (85) 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/index.d.ts +2 -0
  48. package/mcp-server/index.d.ts.map +1 -1
  49. package/mcp-server/index.js +1 -0
  50. package/mcp-server/index.js.map +1 -1
  51. package/mcp-server/mcpServerSetupReducer.d.ts +4 -4
  52. package/mcp-server/useMcpServerCredentials.d.ts +32 -17
  53. package/mcp-server/useMcpServerCredentials.d.ts.map +1 -1
  54. package/mcp-server/useMcpServerCredentials.js +30 -19
  55. package/mcp-server/useMcpServerCredentials.js.map +1 -1
  56. package/mcp-server/useMcpServerSetup.d.ts +5 -5
  57. package/mcp-server/useMcpServerSetup.d.ts.map +1 -1
  58. package/mcp-server/useMcpServerSetup.js +33 -13
  59. package/mcp-server/useMcpServerSetup.js.map +1 -1
  60. package/mcp-server/useOAuthGrantStatus.d.ts +41 -0
  61. package/mcp-server/useOAuthGrantStatus.d.ts.map +1 -0
  62. package/mcp-server/useOAuthGrantStatus.js +91 -0
  63. package/mcp-server/useOAuthGrantStatus.js.map +1 -0
  64. package/package.json +4 -4
  65. package/src/agent/AgentDetailView.tsx +5 -5
  66. package/src/agent/agentSetupReducer.ts +3 -3
  67. package/src/agent/index.ts +1 -1
  68. package/src/agent/useAgentSetup.ts +11 -11
  69. package/src/environment/EnvVarForm.tsx +9 -3
  70. package/src/environment/EnvironmentListPanel.tsx +27 -9
  71. package/src/environment/{diffEnvSpec.ts → diffEnv.ts} +10 -9
  72. package/src/environment/index.ts +1 -1
  73. package/src/execution/SessionVariablesInput.tsx +1 -1
  74. package/src/index.ts +4 -2
  75. package/src/library/parse-resource-yaml.ts +27 -18
  76. package/src/library/serialize-resource-yaml.ts +20 -27
  77. package/src/mcp-server/McpServerDetailView.tsx +39 -7
  78. package/src/mcp-server/McpServerPicker.tsx +9 -1
  79. package/src/mcp-server/index.ts +3 -0
  80. package/src/mcp-server/mcpServerSetupReducer.ts +4 -4
  81. package/src/mcp-server/useMcpServerCredentials.ts +65 -32
  82. package/src/mcp-server/useMcpServerSetup.ts +38 -15
  83. package/src/mcp-server/useOAuthGrantStatus.ts +125 -0
  84. package/environment/diffEnvSpec.d.ts.map +0 -1
  85. package/environment/diffEnvSpec.js.map +0 -1
@@ -9,8 +9,6 @@ import type {
9
9
  StdioServerConfigInput,
10
10
  HttpServerConfigInput,
11
11
  ToolApprovalPolicyInput,
12
- EnvSpecInput,
13
- EnvVarInput,
14
12
  ResourceRef,
15
13
  } from "@stigmer/sdk";
16
14
  import type { StigmerResourceKind } from "./detect-stigmer-resource";
@@ -47,7 +45,7 @@ export type ParsedResource =
47
45
  *
48
46
  * Handles the conversion from proto-style snake_case YAML field names to
49
47
  * the camelCase TypeScript SDK input types, including nested structures
50
- * like `mcp_server_usages`, `sub_agents`, and `env_spec`.
48
+ * like `mcp_server_usages`, `sub_agents`, and `env`.
51
49
  *
52
50
  * The `org` parameter **always overrides** `metadata.org` in the YAML.
53
51
  * This matches the UX intent of "Apply to [my-org]" — the user explicitly
@@ -207,7 +205,7 @@ function buildAgentInput(
207
205
  ...optionalField("mcpServerUsages", extractMcpServerUsages(spec)),
208
206
  ...optionalField("skillRefs", extractResourceRefs(spec, "skill_refs")),
209
207
  ...optionalField("subAgents", extractSubAgents(spec)),
210
- ...optionalField("envSpec", extractEnvSpec(spec)),
208
+ ...optionalField("env", extractEnv(spec)),
211
209
  };
212
210
  }
213
211
 
@@ -333,7 +331,7 @@ function buildMcpServerInput(
333
331
  "pinnedToolApprovals",
334
332
  extractToolApprovalPolicy,
335
333
  ),
336
- ...optionalField("envSpec", extractEnvSpec(spec)),
334
+ ...optionalField("env", extractEnv(spec)),
337
335
  };
338
336
  }
339
337
 
@@ -389,35 +387,46 @@ function extractToolApprovalPolicy(
389
387
  // Shared extractors
390
388
  // ---------------------------------------------------------------------------
391
389
 
392
- function extractEnvSpec(
390
+ function extractEnv(
393
391
  spec: Record<string, unknown>,
394
- ): EnvSpecInput | undefined {
395
- const envSpecRaw = spec.env_spec ?? spec.envSpec;
396
- if (!isPlainObject(envSpecRaw)) return undefined;
392
+ ): Record<string, { isSecret?: boolean; description?: string; optional?: boolean }> | undefined {
393
+ // Support both new flat `env` and legacy nested `env_spec.data` / `envSpec.data`.
394
+ let envMap: Record<string, unknown> | undefined;
395
+
396
+ const envRaw = spec.env;
397
+ if (isPlainObject(envRaw)) {
398
+ envMap = envRaw;
399
+ } else {
400
+ const envSpecRaw = spec.env_spec ?? spec.envSpec;
401
+ if (isPlainObject(envSpecRaw)) {
402
+ const data = envSpecRaw.data ?? envSpecRaw.variables;
403
+ if (isPlainObject(data)) {
404
+ envMap = data;
405
+ }
406
+ }
407
+ }
397
408
 
398
- // Proto uses `data` as the map field name; SDK uses `variables`.
399
- const data = envSpecRaw.data ?? envSpecRaw.variables;
400
- if (!isPlainObject(data)) return undefined;
409
+ if (!envMap) return undefined;
401
410
 
402
- const variables: Record<string, EnvVarInput> = {};
411
+ const result: Record<string, { isSecret?: boolean; description?: string; optional?: boolean }> = {};
403
412
  let hasEntries = false;
404
413
 
405
- for (const [key, val] of Object.entries(data)) {
414
+ for (const [key, val] of Object.entries(envMap)) {
406
415
  if (!isPlainObject(val)) continue;
407
416
 
408
- const value = typeof val.value === "string" ? val.value : "";
409
417
  const isSecret = val.is_secret ?? val.isSecret;
410
418
  const description = val.description;
419
+ const optional = val.optional;
411
420
 
412
- variables[key] = {
413
- value,
421
+ result[key] = {
414
422
  ...(typeof isSecret === "boolean" && { isSecret }),
415
423
  ...(typeof description === "string" && { description }),
424
+ ...(typeof optional === "boolean" && { optional }),
416
425
  };
417
426
  hasEntries = true;
418
427
  }
419
428
 
420
- return hasEntries ? { variables } : undefined;
429
+ return hasEntries ? result : undefined;
421
430
  }
422
431
 
423
432
  function extractResourceRef(raw: Record<string, unknown>): ResourceRef {
@@ -15,7 +15,7 @@ import type {
15
15
  ToolApprovalPolicy,
16
16
  } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
17
17
  import type { ApiResourceReference } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
18
- import type { EnvironmentSpec } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/spec_pb";
18
+ import type { EnvVarDeclaration } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/spec_pb";
19
19
 
20
20
  /**
21
21
  * Serializes a proto `Agent` into the canonical Stigmer YAML format.
@@ -147,10 +147,10 @@ function buildAgentSpec(
147
147
  result.sub_agents = spec.subAgents.map(serializeSubAgent);
148
148
  }
149
149
 
150
- if (spec.envSpec) {
151
- const envSpec = serializeEnvSpec(spec.envSpec);
152
- if (envSpec) {
153
- result.env_spec = envSpec;
150
+ if (hasEntries(spec.env)) {
151
+ const env = serializeEnv(spec.env);
152
+ if (env) {
153
+ result.env = env;
154
154
  }
155
155
  }
156
156
 
@@ -271,10 +271,10 @@ function buildMcpServerSpec(
271
271
  spec.pinnedToolApprovals.map(serializeToolApprovalPolicy);
272
272
  }
273
273
 
274
- if (spec.envSpec) {
275
- const envSpec = serializeEnvSpec(spec.envSpec);
276
- if (envSpec) {
277
- result.env_spec = envSpec;
274
+ if (hasEntries(spec.env)) {
275
+ const env = serializeEnv(spec.env);
276
+ if (env) {
277
+ result.env = env;
278
278
  }
279
279
  }
280
280
 
@@ -361,20 +361,17 @@ function serializeResourceRef(
361
361
  return result;
362
362
  }
363
363
 
364
- function serializeEnvSpec(
365
- envSpec: EnvironmentSpec,
366
- ): Record<string, unknown> | undefined {
367
- if (!hasEntries(envSpec.data)) return undefined;
364
+ function serializeEnv(
365
+ env: { [key: string]: EnvVarDeclaration },
366
+ ): Record<string, Record<string, unknown>> | undefined {
367
+ const keys = Object.keys(env);
368
+ if (keys.length === 0) return undefined;
368
369
 
369
- const data: Record<string, Record<string, unknown>> = {};
370
+ const result: Record<string, Record<string, unknown>> = {};
370
371
 
371
- for (const [key, val] of Object.entries(envSpec.data)) {
372
+ for (const [key, val] of Object.entries(env)) {
372
373
  const entry: Record<string, unknown> = {};
373
374
 
374
- if (val.value) {
375
- entry.value = val.value;
376
- }
377
-
378
375
  if (val.isSecret) {
379
376
  entry.is_secret = val.isSecret;
380
377
  }
@@ -383,17 +380,13 @@ function serializeEnvSpec(
383
380
  entry.description = val.description;
384
381
  }
385
382
 
386
- data[key] = entry;
387
- }
388
-
389
- const result: Record<string, unknown> = {};
383
+ if (val.optional) {
384
+ entry.optional = val.optional;
385
+ }
390
386
 
391
- if (envSpec.description) {
392
- result.description = envSpec.description;
387
+ result[key] = entry;
393
388
  }
394
389
 
395
- result.data = data;
396
-
397
390
  return result;
398
391
  }
399
392
 
@@ -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);
@@ -133,7 +141,7 @@ export function McpServerDetailView({
133
141
  if (!mcpServer?.metadata?.id) return;
134
142
 
135
143
  try {
136
- await oauth.startOAuth(mcpServer.metadata.id, org);
144
+ await oauth.startOAuth(mcpServer.metadata.id, activeOrg ?? org);
137
145
  credentials.refetch();
138
146
  refetch();
139
147
  } catch {
@@ -258,9 +266,9 @@ export function McpServerDetailView({
258
266
  <ServerConfigSection serverType={spec.serverType} />
259
267
  )}
260
268
 
261
- {spec?.envSpec && Object.keys(spec.envSpec.data).length > 0 && (
262
- <EnvSpecSection
263
- data={spec.envSpec.data}
269
+ {spec?.env && Object.keys(spec.env).length > 0 && (
270
+ <EnvSection
271
+ data={spec.env}
264
272
  oauthTargetEnvVar={credentials.oauthTargetEnvVar}
265
273
  />
266
274
  )}
@@ -278,6 +286,7 @@ export function McpServerDetailView({
278
286
  oauthPhase={oauth.phase}
279
287
  authMode={credentials.authMode}
280
288
  isOAuthConnected={credentials.isOAuthConnected}
289
+ accessTokenExpiresAt={credentials.accessTokenExpiresAt}
281
290
  tokenLifetimeHint={credentials.tokenLifetimeHint}
282
291
  />
283
292
 
@@ -346,6 +355,7 @@ function ConnectBar({
346
355
  oauthPhase,
347
356
  authMode,
348
357
  isOAuthConnected,
358
+ accessTokenExpiresAt,
349
359
  tokenLifetimeHint,
350
360
  }: {
351
361
  readonly isConnecting: boolean;
@@ -359,6 +369,7 @@ function ConnectBar({
359
369
  readonly oauthPhase: OAuthConnectPhase;
360
370
  readonly authMode: "manual" | "oauth";
361
371
  readonly isOAuthConnected: boolean;
372
+ readonly accessTokenExpiresAt: bigint;
362
373
  readonly tokenLifetimeHint: string | null;
363
374
  }) {
364
375
  const isOAuthBusy =
@@ -384,6 +395,8 @@ function ConnectBar({
384
395
 
385
396
  const statusText = (() => {
386
397
  if (authMode === "oauth" && isOAuthConnected) {
398
+ const expiryLabel = formatTokenExpiry(accessTokenExpiresAt);
399
+ if (expiryLabel) return `Tokens refresh automatically \u00B7 ${expiryLabel}`;
387
400
  const hint = tokenLifetimeHint && tokenLifetimeHint !== "never"
388
401
  ? ` \u00B7 Session lasts ~${tokenLifetimeHint}`
389
402
  : "";
@@ -480,6 +493,20 @@ function formatConnectionSummary(toolCount: number, policyCount: number): string
480
493
  return `${toolLabel}, ${policyLabel}`;
481
494
  }
482
495
 
496
+ function formatTokenExpiry(expiresAtSeconds: bigint): string | null {
497
+ if (expiresAtSeconds === BigInt(0)) return null;
498
+ const nowSeconds = BigInt(Math.floor(Date.now() / 1000));
499
+ const remainingSeconds = expiresAtSeconds - nowSeconds;
500
+ if (remainingSeconds <= BigInt(0)) return "Token expired";
501
+ const minutes = Number(remainingSeconds / BigInt(60));
502
+ if (minutes < 1) return "Expires in <1 min";
503
+ if (minutes < 60) return `Expires in ${minutes} min`;
504
+ const hours = Math.floor(minutes / 60);
505
+ if (hours < 24) return `Expires in ${hours}h ${minutes % 60}m`;
506
+ const days = Math.floor(hours / 24);
507
+ return `Expires in ${days}d`;
508
+ }
509
+
483
510
  // ---------------------------------------------------------------------------
484
511
  // Internal section components
485
512
  // ---------------------------------------------------------------------------
@@ -760,11 +787,11 @@ function ResourceTemplatesList({
760
787
  );
761
788
  }
762
789
 
763
- function EnvSpecSection({
790
+ function EnvSection({
764
791
  data,
765
792
  oauthTargetEnvVar,
766
793
  }: {
767
- readonly data: { [key: string]: EnvironmentValue };
794
+ readonly data: { [key: string]: EnvVarDeclaration };
768
795
  readonly oauthTargetEnvVar: string | null;
769
796
  }) {
770
797
  const entries = Object.entries(data).sort(([a], [b]) =>
@@ -789,6 +816,11 @@ function EnvSpecSection({
789
816
  oauth
790
817
  </span>
791
818
  )}
819
+ {env.optional && (
820
+ <span className="shrink-0 rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground/70">
821
+ optional
822
+ </span>
823
+ )}
792
824
  {env.description && (
793
825
  <span className="text-xs text-muted-foreground">
794
826
  {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
  | {
@@ -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
  }