@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
@@ -26,7 +26,7 @@ export type {
26
26
  AgentEnvFormVariable,
27
27
  } from "./AgentEnvForm";
28
28
 
29
- export { diffEnvSpec } from "../environment/diffEnvSpec";
29
+ export { diffEnv } from "../environment/diffEnv";
30
30
 
31
31
  export { useAgentSetup } from "./useAgentSetup";
32
32
  export type {
@@ -10,7 +10,7 @@ import { useStigmer } from "../hooks";
10
10
  import { toError } from "../internal/toError";
11
11
  import { usePersonalEnvironment } from "../environment/usePersonalEnvironment";
12
12
  import { buildPersonalInstanceInput } from "../agent-instance/buildPersonalInstanceInput";
13
- import { diffEnvSpec } from "../environment/diffEnvSpec";
13
+ import { diffEnv } from "../environment/diffEnv";
14
14
  import {
15
15
  agentSetupReducer,
16
16
  INITIAL_STATE,
@@ -106,8 +106,8 @@ export interface UseAgentSetupReturn {
106
106
  /**
107
107
  * Evaluate whether an agent is ready to use or needs env var collection.
108
108
  *
109
- * Fetches the full agent to read its `env_spec`, checks for an existing
110
- * personal instance, and diffs env_spec keys against the personal
109
+ * Fetches the full agent to read its `env` declarations, checks for an
110
+ * existing personal instance, and diffs env keys against the personal
111
111
  * environment. Returns `"ready"` when the agent can be used immediately,
112
112
  * or `"needsEnvVars"` when the caller should present {@link AgentEnvForm}.
113
113
  */
@@ -143,7 +143,7 @@ export interface UseAgentSetupReturn {
143
143
  *
144
144
  * When a user picks an agent in the {@link AgentPicker}, this hook
145
145
  * determines whether the agent requires credentials (via its
146
- * `env_spec`), checks what the user has already provided in their
146
+ * `env` declarations), checks what the user has already provided in their
147
147
  * personal environment, and either reports the agent as ready or
148
148
  * identifies the missing variables so the caller can render
149
149
  * {@link AgentEnvForm}.
@@ -173,7 +173,7 @@ export interface UseAgentSetupReturn {
173
173
  * @param org - Organization slug. Pass `null` to disable.
174
174
  * @param poolKeys - Optional set of env-var keys already available
175
175
  * from the session env pool (manual secrets, one-time env vars from
176
- * other components). When provided, agents whose `env_spec` keys
176
+ * other components). When provided, agents whose `env` keys
177
177
  * are fully covered by `poolKeys` + personal env auto-resolve to
178
178
  * `ready` without prompting. Reactive — when `poolKeys` changes,
179
179
  * `needsEnvVars` is re-evaluated.
@@ -221,10 +221,10 @@ export function useAgentSetup(
221
221
  try {
222
222
  const agent = await stigmer.agent.getByReference(ref);
223
223
  const agentName = agent.metadata?.name ?? ref.slug;
224
- const envSpecData = agent.spec?.envSpec?.data;
224
+ const envDeclarations = agent.spec?.env;
225
225
 
226
- // No env_spec — agent is immediately ready (direct mode).
227
- if (!envSpecData || Object.keys(envSpecData).length === 0) {
226
+ // No env declarations — agent is immediately ready (direct mode).
227
+ if (!envDeclarations || Object.keys(envDeclarations).length === 0) {
228
228
  const resolution: AgentResolution = { mode: "direct" };
229
229
  dispatch({
230
230
  type: "RESOLVE_READY",
@@ -235,7 +235,7 @@ export function useAgentSetup(
235
235
  return { status: "ready", agentRef: ref, agentName, resolution };
236
236
  }
237
237
 
238
- // Agent has env_spec — check for existing personal instance.
238
+ // Agent has env declarations — check for existing personal instance.
239
239
  const agentLabel = `${ref.org}/${ref.slug}`;
240
240
  const instanceList = await stigmer.agentInstance.list(
241
241
  create(ListAgentInstancesRequestSchema, {
@@ -265,8 +265,8 @@ export function useAgentSetup(
265
265
  const existingKeys = new Set(
266
266
  Object.keys(personalEnv.environment?.spec?.data ?? {}),
267
267
  );
268
- const personalOnlyMissing = diffEnvSpec(envSpecData, existingKeys);
269
- const missingVariables = diffEnvSpec(envSpecData, existingKeys, poolKeys);
268
+ const personalOnlyMissing = diffEnv(envDeclarations, existingKeys);
269
+ const missingVariables = diffEnv(envDeclarations, existingKeys, poolKeys);
270
270
 
271
271
  if (personalOnlyMissing.length === 0) {
272
272
  // Personal env covers all keys — create personal instance.
@@ -9,7 +9,7 @@ import { ScrollFade } from "../internal/ScrollFade";
9
9
  /**
10
10
  * Describes a single environment variable the form should collect.
11
11
  *
12
- * Typically derived from a resource's `env_spec.data` entries (Agent,
12
+ * Typically derived from a resource's `env` entries (Agent,
13
13
  * McpServer, or any resource that declares required environment
14
14
  * variables). The caller is responsible for filtering out variables
15
15
  * the user has already provided (i.e. only pass the *missing* ones).
@@ -19,8 +19,14 @@ export interface EnvVarFormVariable {
19
19
  readonly key: string;
20
20
  /** When true, the input renders as a password field with a visibility toggle. */
21
21
  readonly isSecret: boolean;
22
- /** Help text shown below the input. From the resource's env_spec description. */
22
+ /** Help text shown below the input. From the resource's env declaration description. */
23
23
  readonly description?: string;
24
+ /**
25
+ * When true, this variable is not required for the resource to function.
26
+ * Callers can use this to filter optional vars out of forms or to show
27
+ * them separately from required vars.
28
+ */
29
+ readonly optional?: boolean;
24
30
  }
25
31
 
26
32
  /** Options reported by the form alongside the collected values on submit. */
@@ -110,7 +116,7 @@ export interface EnvVarFormProps {
110
116
 
111
117
  /**
112
118
  * Compact form that collects environment variable values for any
113
- * resource that declares an `env_spec` (Agents, MCP servers, etc.).
119
+ * resource that declares `env` variables (Agents, MCP servers, etc.).
114
120
  *
115
121
  * Renders one labeled input per variable. Secret variables use a
116
122
  * password field with a visibility toggle. The form validates that
@@ -18,11 +18,26 @@ export interface EnvironmentListPanelProps {
18
18
  /** Optional label filter — only environments matching ALL labels are shown. */
19
19
  readonly labels?: Record<string, string>;
20
20
  /**
21
- * Exclude environments whose labels contain all key-value pairs in this
22
- * record. Useful for filtering the personal environment out of the
23
- * organization list: `excludeLabels={{ "stigmer.ai/personal": "true" }}`.
21
+ * Exclude environments whose labels match one or more label sets.
22
+ *
23
+ * A single record uses AND semantics — the environment must match
24
+ * **all** key-value pairs to be excluded. Pass an array of records
25
+ * for OR-of-AND semantics: the environment is excluded when **any**
26
+ * record fully matches.
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * // Exclude personal envs only (single record — backward-compatible)
31
+ * excludeLabels={{ "stigmer.ai/personal": "true" }}
32
+ *
33
+ * // Exclude personal AND managed envs (array of records)
34
+ * excludeLabels={[
35
+ * { "stigmer.ai/personal": "true" },
36
+ * { "stigmer.ai/managed": "true" },
37
+ * ]}
38
+ * ```
24
39
  */
25
- readonly excludeLabels?: Record<string, string>;
40
+ readonly excludeLabels?: Record<string, string> | Record<string, string>[];
26
41
  /** Fired when a user selects (expands) an environment. */
27
42
  readonly onEnvironmentSelect?: (env: Environment) => void;
28
43
  /** When `true`, variable editors render in read-only mode. */
@@ -82,13 +97,16 @@ export function EnvironmentListPanel({
82
97
  }
83
98
 
84
99
  const filtered = useMemo(() => {
85
- if (!excludeLabels || Object.keys(excludeLabels).length === 0) {
86
- return environments;
87
- }
100
+ if (!excludeLabels) return environments;
101
+ const labelSets = Array.isArray(excludeLabels)
102
+ ? excludeLabels
103
+ : [excludeLabels];
104
+ if (labelSets.length === 0) return environments;
105
+
88
106
  return environments.filter((env) => {
89
107
  const envLabels = env.metadata?.labels ?? {};
90
- const shouldExclude = Object.entries(excludeLabels).every(
91
- ([k, v]) => envLabels[k] === v,
108
+ const shouldExclude = labelSets.some((labelSet) =>
109
+ Object.entries(labelSet).every(([k, v]) => envLabels[k] === v),
92
110
  );
93
111
  return !shouldExclude;
94
112
  });
@@ -4,38 +4,38 @@ import type { EnvVarFormVariable } from "./EnvVarForm";
4
4
  * Computes the list of environment variables a resource requires that
5
5
  * the user has not yet provided.
6
6
  *
7
- * Compares a resource's declared `env_spec.data` keys against a set of
7
+ * Compares a resource's declared `env` keys against a set of
8
8
  * keys already present in the user's personal environment **and** an
9
9
  * optional session-level env pool (manual secrets, one-time env vars
10
10
  * from other components). Variables whose keys are missing from both
11
11
  * sources are returned as {@link EnvVarFormVariable} entries suitable
12
12
  * for rendering in {@link EnvVarForm}.
13
13
  *
14
- * Works with any resource that declares an `EnvironmentSpec` — Agents,
14
+ * Works with any resource that declares env var declarations — Agents,
15
15
  * MCP servers, or future resource types.
16
16
  *
17
17
  * This is a pure function with no side effects — it can be unit-tested
18
18
  * independently of hooks, providers, or API calls.
19
19
  *
20
- * @param envSpecData - The resource's `spec.envSpec.data` record.
21
- * Each entry declares a variable the resource needs, with `isSecret`
22
- * and an optional `description`.
20
+ * @param envDeclarations - The resource's `spec.env` record.
21
+ * Each entry declares a variable the resource needs, with `isSecret`,
22
+ * an optional `description`, and an `optional` flag.
23
23
  * @param existingKeys - Keys already present in the user's personal
24
24
  * environment (or any environment being checked against).
25
25
  * @param poolKeys - Optional additional keys from the session env pool
26
26
  * (manual secrets, one-time env vars from other agents/MCP servers).
27
27
  * When provided, variables satisfied by the pool are also excluded
28
28
  * from the "missing" list.
29
- * @returns Variables from `envSpecData` not found in either key set.
29
+ * @returns Variables from `envDeclarations` not found in either key set.
30
30
  */
31
- export function diffEnvSpec(
32
- envSpecData: Record<string, { isSecret: boolean; description?: string }>,
31
+ export function diffEnv(
32
+ envDeclarations: Record<string, { isSecret: boolean; description?: string; optional?: boolean }>,
33
33
  existingKeys: Set<string>,
34
34
  poolKeys?: Set<string>,
35
35
  ): EnvVarFormVariable[] {
36
36
  const missing: EnvVarFormVariable[] = [];
37
37
 
38
- for (const [key, value] of Object.entries(envSpecData)) {
38
+ for (const [key, value] of Object.entries(envDeclarations)) {
39
39
  if (existingKeys.has(key)) continue;
40
40
  if (poolKeys?.has(key)) continue;
41
41
 
@@ -43,6 +43,7 @@ export function diffEnvSpec(
43
43
  key,
44
44
  isSecret: value.isSecret,
45
45
  ...(value.description && { description: value.description }),
46
+ ...(value.optional && { optional: true }),
46
47
  });
47
48
  }
48
49
 
@@ -35,7 +35,7 @@ export type {
35
35
  EnvVarFormVariable,
36
36
  EnvVarFormSubmitOptions,
37
37
  } from "./EnvVarForm";
38
- export { diffEnvSpec } from "./diffEnvSpec";
38
+ export { diffEnv } from "./diffEnv";
39
39
  export { useSessionEnvPool } from "./useSessionEnvPool";
40
40
  export type {
41
41
  SessionEnvPoolInput,
@@ -22,7 +22,7 @@ export interface SessionVariablesInputProps {
22
22
  * between the variable and the agent/MCP server that needs it.
23
23
  *
24
24
  * Built by `SessionComposer` from the selected agent's and MCP
25
- * servers' `env_spec` declarations.
25
+ * servers' `env` declarations.
26
26
  *
27
27
  * @example
28
28
  * ```ts
package/src/index.ts CHANGED
@@ -221,6 +221,7 @@ export {
221
221
  useMcpServerConnect,
222
222
  useMcpServerOAuthConnect,
223
223
  useMcpServerCredentials,
224
+ useOAuthGrantStatus,
224
225
  OAuthCallbackHandler,
225
226
  McpServerPicker,
226
227
  McpServerConfigPanel,
@@ -255,6 +256,7 @@ export type {
255
256
  OAuthCallbackHandlerProps,
256
257
  OAuthCallbackParams,
257
258
  UseMcpServerCredentialsReturn,
259
+ UseOAuthGrantStatusReturn,
258
260
  McpServerAuthMode,
259
261
  } from "./mcp-server";
260
262
 
@@ -298,7 +300,7 @@ export type {
298
300
  GitHubRepoPickerProps,
299
301
  } from "./github";
300
302
 
301
- // Agent — data hook, count hook, list hook, search hook, picker, detail view, env form, setup orchestration, env_spec diffing, and default agent
303
+ // Agent — data hook, count hook, list hook, search hook, picker, detail view, env form, setup orchestration, env diffing, and default agent
302
304
  export {
303
305
  useAgent,
304
306
  useAgentCount,
@@ -307,7 +309,7 @@ export {
307
309
  AgentPicker,
308
310
  AgentDetailView,
309
311
  AgentEnvForm,
310
- diffEnvSpec,
312
+ diffEnv,
311
313
  useAgentSetup,
312
314
  useDefaultAgent,
313
315
  } from "./agent";
@@ -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);
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);
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
@@ -7,7 +7,9 @@ import type { OAuthCallbackMessage } from "./useMcpServerOAuthConnect";
7
7
 
8
8
  /** Parameters extracted from the OAuth callback URL. */
9
9
  export interface OAuthCallbackParams {
10
+ /** The authorization code returned by the OAuth provider. */
10
11
  readonly code: string;
12
+ /** The opaque state token used to correlate the callback with the originating request. */
11
13
  readonly state: string;
12
14
  }
13
15
 
@@ -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
  | {