@agent-native/dispatch 0.8.4 → 0.8.6

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 (83) hide show
  1. package/dist/actions/ask_app.d.ts +3 -0
  2. package/dist/actions/ask_app.d.ts.map +1 -0
  3. package/dist/actions/ask_app.js +12 -0
  4. package/dist/actions/ask_app.js.map +1 -0
  5. package/dist/actions/create_embed_session.d.ts +3 -0
  6. package/dist/actions/create_embed_session.d.ts.map +1 -0
  7. package/dist/actions/create_embed_session.js +28 -0
  8. package/dist/actions/create_embed_session.js.map +1 -0
  9. package/dist/actions/index.d.ts.map +1 -1
  10. package/dist/actions/index.js +12 -0
  11. package/dist/actions/index.js.map +1 -1
  12. package/dist/actions/list-mcp-app-access.d.ts +3 -0
  13. package/dist/actions/list-mcp-app-access.d.ts.map +1 -0
  14. package/dist/actions/list-mcp-app-access.js +25 -0
  15. package/dist/actions/list-mcp-app-access.js.map +1 -0
  16. package/dist/actions/list_apps.d.ts +3 -0
  17. package/dist/actions/list_apps.d.ts.map +1 -0
  18. package/dist/actions/list_apps.js +26 -0
  19. package/dist/actions/list_apps.js.map +1 -0
  20. package/dist/actions/open_app.d.ts +3 -0
  21. package/dist/actions/open_app.d.ts.map +1 -0
  22. package/dist/actions/open_app.js +59 -0
  23. package/dist/actions/open_app.js.map +1 -0
  24. package/dist/actions/set-mcp-app-access.d.ts +3 -0
  25. package/dist/actions/set-mcp-app-access.d.ts.map +1 -0
  26. package/dist/actions/set-mcp-app-access.js +46 -0
  27. package/dist/actions/set-mcp-app-access.js.map +1 -0
  28. package/dist/actions/start-workspace-app-creation.js +1 -1
  29. package/dist/actions/start-workspace-app-creation.js.map +1 -1
  30. package/dist/actions/view-screen.d.ts.map +1 -1
  31. package/dist/actions/view-screen.js +8 -0
  32. package/dist/actions/view-screen.js.map +1 -1
  33. package/dist/components/create-app-popover.d.ts.map +1 -1
  34. package/dist/components/create-app-popover.js +1 -0
  35. package/dist/components/create-app-popover.js.map +1 -1
  36. package/dist/routes/pages/agents.d.ts.map +1 -1
  37. package/dist/routes/pages/agents.js +74 -3
  38. package/dist/routes/pages/agents.js.map +1 -1
  39. package/dist/routes/pages/apps.d.ts.map +1 -1
  40. package/dist/routes/pages/apps.js +23 -8
  41. package/dist/routes/pages/apps.js.map +1 -1
  42. package/dist/routes/pages/overview.d.ts.map +1 -1
  43. package/dist/routes/pages/overview.js +1 -3
  44. package/dist/routes/pages/overview.js.map +1 -1
  45. package/dist/server/lib/mcp-access-store.d.ts +16 -0
  46. package/dist/server/lib/mcp-access-store.d.ts.map +1 -0
  47. package/dist/server/lib/mcp-access-store.js +64 -0
  48. package/dist/server/lib/mcp-access-store.js.map +1 -0
  49. package/dist/server/lib/mcp-gateway.d.ts +47 -0
  50. package/dist/server/lib/mcp-gateway.d.ts.map +1 -0
  51. package/dist/server/lib/mcp-gateway.js +237 -0
  52. package/dist/server/lib/mcp-gateway.js.map +1 -0
  53. package/dist/server/lib/vault-store.d.ts.map +1 -1
  54. package/dist/server/lib/vault-store.js +20 -10
  55. package/dist/server/lib/vault-store.js.map +1 -1
  56. package/dist/server/plugins/agent-chat.d.ts.map +1 -1
  57. package/dist/server/plugins/agent-chat.js +1 -0
  58. package/dist/server/plugins/agent-chat.js.map +1 -1
  59. package/dist/server/plugins/integrations.d.ts.map +1 -1
  60. package/dist/server/plugins/integrations.js +2 -1
  61. package/dist/server/plugins/integrations.js.map +1 -1
  62. package/package.json +1 -1
  63. package/src/actions/ask_app.ts +13 -0
  64. package/src/actions/create_embed_session.ts +29 -0
  65. package/src/actions/index.spec.ts +6 -0
  66. package/src/actions/index.ts +12 -0
  67. package/src/actions/list-mcp-app-access.ts +26 -0
  68. package/src/actions/list_apps.ts +27 -0
  69. package/src/actions/open_app.ts +61 -0
  70. package/src/actions/set-mcp-app-access.ts +59 -0
  71. package/src/actions/start-workspace-app-creation.ts +1 -1
  72. package/src/actions/view-screen.ts +8 -0
  73. package/src/components/create-app-popover.tsx +1 -0
  74. package/src/routes/pages/agents.tsx +187 -5
  75. package/src/routes/pages/apps.tsx +209 -67
  76. package/src/routes/pages/overview.tsx +16 -10
  77. package/src/server/lib/mcp-access-store.spec.ts +58 -0
  78. package/src/server/lib/mcp-access-store.ts +104 -0
  79. package/src/server/lib/mcp-gateway.ts +333 -0
  80. package/src/server/lib/vault-store.spec.ts +15 -0
  81. package/src/server/lib/vault-store.ts +30 -7
  82. package/src/server/plugins/agent-chat.ts +1 -0
  83. package/src/server/plugins/integrations.ts +2 -1
@@ -5,6 +5,7 @@ import {
5
5
  IconBrush,
6
6
  IconCalendarMonth,
7
7
  IconChartBar,
8
+ IconChevronDown,
8
9
  IconClipboardList,
9
10
  IconEyeOff,
10
11
  IconFileText,
@@ -22,6 +23,13 @@ import { CreateAppPopover } from "@/components/create-app-popover";
22
23
  import { DispatchShell } from "@/components/dispatch-shell";
23
24
  import { WorkspaceAppCard } from "@/components/workspace-app-card";
24
25
  import { Button } from "@/components/ui/button";
26
+ import {
27
+ Collapsible,
28
+ CollapsibleContent,
29
+ CollapsibleTrigger,
30
+ } from "@/components/ui/collapsible";
31
+ import { Skeleton } from "@/components/ui/skeleton";
32
+ import { cn } from "@/lib/utils";
25
33
  import type { WorkspaceAppSummary } from "@/lib/workspace-apps";
26
34
 
27
35
  export function meta() {
@@ -58,7 +66,8 @@ const TEMPLATE_ICONS: Record<string, typeof IconMail> = {
58
66
 
59
67
  export default function AppsRoute() {
60
68
  const [showHidden, setShowHidden] = useState(false);
61
- const { data: apps = [] } = useActionQuery(
69
+ const [templatesOpen, setTemplatesOpen] = useState(false);
70
+ const { data: apps = [], isLoading: appsLoading } = useActionQuery(
62
71
  "list-workspace-apps",
63
72
  { includeAgentCards: false, includeArchived: true },
64
73
  {
@@ -70,7 +79,7 @@ export default function AppsRoute() {
70
79
  {},
71
80
  { staleTime: 60_000 },
72
81
  );
73
- const { data: templates = [] } = useActionQuery(
82
+ const { data: templates = [], isLoading: templatesLoading } = useActionQuery(
74
83
  "list-available-workspace-templates",
75
84
  {},
76
85
  { refetchInterval: 5_000 },
@@ -84,6 +93,7 @@ export default function AppsRoute() {
84
93
  const visibleApps = allApps.filter((app) => !app.archived);
85
94
  const archivedApps = allApps.filter((app) => app.archived);
86
95
  const typedTemplates = templates as AvailableTemplate[];
96
+ const showAppSkeletons = appsLoading && allApps.length === 0;
87
97
 
88
98
  return (
89
99
  <DispatchShell
@@ -94,82 +104,214 @@ export default function AppsRoute() {
94
104
  : "Open workspace apps and start new app creation from Dispatch."
95
105
  }
96
106
  >
97
- <div className="space-y-6">
107
+ <div className="space-y-8">
98
108
  <section className="space-y-3">
99
- <div className="flex items-center justify-between gap-3">
100
- <div className="flex items-center gap-2">
101
- <IconApps size={16} className="text-muted-foreground" />
102
- <h2 className="text-sm font-semibold text-foreground">
103
- {workspaceLabel
104
- ? `Apps in ${workspaceLabel}`
105
- : "Workspace apps"}
106
- </h2>
109
+ <div className="flex flex-wrap items-end justify-between gap-3">
110
+ <div className="flex min-w-0 items-start gap-2">
111
+ <IconApps
112
+ size={16}
113
+ className="mt-0.5 shrink-0 text-muted-foreground"
114
+ />
115
+ <div className="min-w-0">
116
+ <h2 className="truncate text-sm font-semibold text-foreground">
117
+ {workspaceLabel
118
+ ? `Apps in ${workspaceLabel}`
119
+ : "Workspace apps"}
120
+ </h2>
121
+ <p className="mt-0.5 text-xs text-muted-foreground">
122
+ {visibleApps.length} active
123
+ {archivedApps.length > 0
124
+ ? ` · ${archivedApps.length} hidden`
125
+ : ""}
126
+ </p>
127
+ </div>
107
128
  </div>
108
- <CreateAppPopover
109
- align="end"
110
- trigger={
111
- <Button size="sm" variant="outline">
112
- <IconPlus size={15} className="mr-1.5" />
113
- App
114
- </Button>
115
- }
116
- />
117
- </div>
118
-
119
- <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
120
- {visibleApps.map((app) => (
121
- <WorkspaceAppCard key={app.id} app={app} />
122
- ))}
123
-
124
- <CreateAppPopover />
129
+ {visibleApps.length > 0 ? (
130
+ <CreateAppPopover
131
+ align="end"
132
+ trigger={
133
+ <Button size="sm">
134
+ <IconPlus size={15} className="mr-1.5" />
135
+ Create app
136
+ </Button>
137
+ }
138
+ />
139
+ ) : null}
125
140
  </div>
126
- </section>
127
141
 
128
- {typedTemplates.length > 0 ? (
129
- <section className="space-y-3">
130
- <div className="flex items-center gap-2">
131
- <IconStack3 size={16} className="text-muted-foreground" />
132
- <h2 className="text-sm font-semibold text-foreground">
133
- Add a template
134
- </h2>
135
- <span className="text-xs text-muted-foreground">
136
- Scaffold a first-party app into{" "}
137
- <code className="font-mono text-[11px]">apps/</code>.
138
- </span>
139
- </div>
140
- <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
141
- {typedTemplates.map((template) => (
142
- <AddTemplateCard key={template.name} template={template} />
142
+ {showAppSkeletons ? (
143
+ <AppsSkeletonGrid />
144
+ ) : visibleApps.length > 0 ? (
145
+ <div className="grid auto-rows-fr gap-3 md:grid-cols-2 xl:grid-cols-3">
146
+ {visibleApps.map((app) => (
147
+ <WorkspaceAppCard key={app.id} app={app} className="h-full" />
143
148
  ))}
144
149
  </div>
145
- </section>
150
+ ) : (
151
+ <EmptyAppsState />
152
+ )}
153
+ </section>
154
+
155
+ {typedTemplates.length > 0 || templatesLoading ? (
156
+ <Collapsible open={templatesOpen} onOpenChange={setTemplatesOpen}>
157
+ <section className="space-y-3">
158
+ <div className="flex flex-wrap items-center justify-between gap-3 border-t pt-4">
159
+ <div className="flex min-w-0 items-center gap-2">
160
+ <IconStack3
161
+ size={16}
162
+ className="shrink-0 text-muted-foreground"
163
+ />
164
+ <div className="min-w-0">
165
+ <h2 className="text-sm font-semibold text-foreground">
166
+ Templates
167
+ </h2>
168
+ <p className="text-xs text-muted-foreground">
169
+ {templatesLoading
170
+ ? "Checking available templates"
171
+ : `${typedTemplates.length} available to scaffold`}
172
+ </p>
173
+ </div>
174
+ </div>
175
+ <CollapsibleTrigger asChild>
176
+ <Button
177
+ type="button"
178
+ variant="outline"
179
+ size="sm"
180
+ className="gap-1.5"
181
+ >
182
+ {templatesOpen ? "Hide" : "Show"}
183
+ <IconChevronDown
184
+ size={14}
185
+ className={cn(
186
+ "transition-transform",
187
+ templatesOpen && "rotate-180",
188
+ )}
189
+ />
190
+ </Button>
191
+ </CollapsibleTrigger>
192
+ </div>
193
+ <CollapsibleContent>
194
+ {templatesLoading && typedTemplates.length === 0 ? (
195
+ <AppsSkeletonGrid />
196
+ ) : (
197
+ <div className="grid auto-rows-fr gap-3 md:grid-cols-2 xl:grid-cols-3">
198
+ {typedTemplates.map((template) => (
199
+ <AddTemplateCard
200
+ key={template.name}
201
+ template={template}
202
+ />
203
+ ))}
204
+ </div>
205
+ )}
206
+ </CollapsibleContent>
207
+ </section>
208
+ </Collapsible>
146
209
  ) : null}
147
210
 
148
211
  {archivedApps.length > 0 ? (
149
- <section className="space-y-3">
150
- <button
151
- type="button"
152
- onClick={() => setShowHidden((cur) => !cur)}
153
- className="inline-flex cursor-pointer items-center gap-2 text-xs text-muted-foreground hover:text-foreground"
154
- >
155
- <IconEyeOff size={14} />
156
- {showHidden ? "Hide" : "Show"} {archivedApps.length} hidden{" "}
157
- {archivedApps.length === 1 ? "app" : "apps"}
158
- </button>
159
- {showHidden ? (
160
- <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
161
- {archivedApps.map((app) => (
162
- <WorkspaceAppCard key={app.id} app={app} />
163
- ))}
212
+ <Collapsible open={showHidden} onOpenChange={setShowHidden}>
213
+ <section className="space-y-3">
214
+ <div className="flex flex-wrap items-center justify-between gap-3 border-t pt-4">
215
+ <div className="flex min-w-0 items-center gap-2">
216
+ <IconEyeOff
217
+ size={16}
218
+ className="shrink-0 text-muted-foreground"
219
+ />
220
+ <div className="min-w-0">
221
+ <h2 className="text-sm font-semibold text-foreground">
222
+ Hidden apps
223
+ </h2>
224
+ <p className="text-xs text-muted-foreground">
225
+ {archivedApps.length} hidden{" "}
226
+ {archivedApps.length === 1 ? "app" : "apps"}
227
+ </p>
228
+ </div>
229
+ </div>
230
+ <CollapsibleTrigger asChild>
231
+ <Button
232
+ type="button"
233
+ variant="outline"
234
+ size="sm"
235
+ className="gap-1.5"
236
+ >
237
+ {showHidden ? "Hide" : "Show"}
238
+ <IconChevronDown
239
+ size={14}
240
+ className={cn(
241
+ "transition-transform",
242
+ showHidden && "rotate-180",
243
+ )}
244
+ />
245
+ </Button>
246
+ </CollapsibleTrigger>
164
247
  </div>
165
- ) : null}
166
- </section>
248
+ <CollapsibleContent>
249
+ <div className="grid auto-rows-fr gap-3 md:grid-cols-2 xl:grid-cols-3">
250
+ {archivedApps.map((app) => (
251
+ <WorkspaceAppCard
252
+ key={app.id}
253
+ app={app}
254
+ className="h-full"
255
+ />
256
+ ))}
257
+ </div>
258
+ </CollapsibleContent>
259
+ </section>
260
+ </Collapsible>
167
261
  ) : null}
168
262
  </div>
169
263
  </DispatchShell>
170
264
  );
171
265
  }
172
266
 
267
+ function AppsSkeletonGrid() {
268
+ return (
269
+ <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
270
+ {Array.from({ length: 3 }).map((_, index) => (
271
+ <div key={index} className="rounded-lg border bg-card p-4">
272
+ <div className="flex items-start justify-between gap-3">
273
+ <div className="min-w-0 flex-1 space-y-3">
274
+ <Skeleton className="h-4 w-32" />
275
+ <Skeleton className="h-3 w-24" />
276
+ <div className="space-y-2 pt-1">
277
+ <Skeleton className="h-3 w-full" />
278
+ <Skeleton className="h-3 w-2/3" />
279
+ </div>
280
+ </div>
281
+ <Skeleton className="h-7 w-7 rounded-md" />
282
+ </div>
283
+ </div>
284
+ ))}
285
+ </div>
286
+ );
287
+ }
288
+
289
+ function EmptyAppsState() {
290
+ return (
291
+ <div className="rounded-lg border border-dashed bg-card px-4 py-10 text-center">
292
+ <div className="mx-auto flex size-10 items-center justify-center rounded-lg bg-muted text-muted-foreground">
293
+ <IconApps size={18} />
294
+ </div>
295
+ <h3 className="mt-3 text-sm font-semibold text-foreground">
296
+ No workspace apps yet
297
+ </h3>
298
+ <p className="mx-auto mt-1 max-w-sm text-sm text-muted-foreground">
299
+ Create an app when a workflow needs its own focused place to live.
300
+ </p>
301
+ <div className="mt-4">
302
+ <CreateAppPopover
303
+ trigger={
304
+ <Button size="sm">
305
+ <IconPlus size={15} className="mr-1.5" />
306
+ Create app
307
+ </Button>
308
+ }
309
+ />
310
+ </div>
311
+ </div>
312
+ );
313
+ }
314
+
173
315
  function AddTemplateCard({ template }: { template: AvailableTemplate }) {
174
316
  const Icon = TEMPLATE_ICONS[template.icon] ?? IconSparkles;
175
317
  const scaffold = useActionMutation("scaffold-workspace-app", {
@@ -188,9 +330,9 @@ function AddTemplateCard({ template }: { template: AvailableTemplate }) {
188
330
  });
189
331
 
190
332
  return (
191
- <div className="group relative flex items-start gap-3 rounded-lg border bg-card p-4 transition hover:border-foreground/30">
333
+ <div className="group relative flex h-full min-h-36 items-stretch gap-3 rounded-lg border bg-card p-4 transition hover:border-foreground/30">
192
334
  <div
193
- className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md"
335
+ className="flex h-9 w-9 shrink-0 items-center justify-center self-start rounded-md"
194
336
  style={{
195
337
  backgroundColor: `rgb(${template.colorRgb} / 0.12)`,
196
338
  color: template.color,
@@ -198,7 +340,7 @@ function AddTemplateCard({ template }: { template: AvailableTemplate }) {
198
340
  >
199
341
  <Icon size={18} />
200
342
  </div>
201
- <div className="min-w-0 flex-1">
343
+ <div className="flex min-w-0 flex-1 flex-col">
202
344
  <div className="flex min-w-0 items-center gap-2">
203
345
  <h3 className="truncate text-sm font-semibold text-foreground">
204
346
  {template.label}
@@ -207,7 +349,7 @@ function AddTemplateCard({ template }: { template: AvailableTemplate }) {
207
349
  <p className="mt-1 line-clamp-2 text-xs leading-relaxed text-muted-foreground">
208
350
  {template.hint}
209
351
  </p>
210
- <div className="mt-3">
352
+ <div className="mt-auto pt-3">
211
353
  <Button
212
354
  size="sm"
213
355
  variant="outline"
@@ -217,7 +359,7 @@ function AddTemplateCard({ template }: { template: AvailableTemplate }) {
217
359
  {scaffold.isPending ? (
218
360
  <>
219
361
  <IconLoader2 size={14} className="mr-1.5 animate-spin" />
220
- Adding
362
+ Adding...
221
363
  </>
222
364
  ) : (
223
365
  <>
@@ -213,16 +213,22 @@ function WorkspaceAppsSection({
213
213
  </Button>
214
214
  </div>
215
215
 
216
- <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
217
- {showSkeletons
218
- ? Array.from({ length: 6 }).map((_, index) => (
219
- <AppCardSkeleton key={index} />
220
- ))
221
- : visibleApps.map((app) => (
222
- <WorkspaceAppCard key={app.id} app={app} className="min-h-32" />
223
- ))}
224
-
225
- {!showSkeletons ? <CreateAppPopover /> : null}
216
+ <div className="grid auto-rows-fr gap-3 sm:grid-cols-2 xl:grid-cols-3">
217
+ {showSkeletons ? (
218
+ Array.from({ length: 6 }).map((_, index) => (
219
+ <AppCardSkeleton key={index} />
220
+ ))
221
+ ) : visibleApps.length > 0 ? (
222
+ visibleApps.map((app) => (
223
+ <WorkspaceAppCard
224
+ key={app.id}
225
+ app={app}
226
+ className="h-full min-h-32"
227
+ />
228
+ ))
229
+ ) : (
230
+ <CreateAppPopover />
231
+ )}
226
232
  </div>
227
233
  </section>
228
234
  );
@@ -0,0 +1,58 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ isAppAllowedByMcpAccess,
4
+ normalizeMcpAppAccessSettings,
5
+ } from "./mcp-access-store.js";
6
+
7
+ describe("normalizeMcpAppAccessSettings", () => {
8
+ it("defaults to all apps", () => {
9
+ expect(normalizeMcpAppAccessSettings(null)).toEqual({
10
+ mode: "all-apps",
11
+ selectedAppIds: [],
12
+ updatedAt: undefined,
13
+ updatedBy: undefined,
14
+ });
15
+ });
16
+
17
+ it("normalizes selected app ids", () => {
18
+ expect(
19
+ normalizeMcpAppAccessSettings({
20
+ mode: "selected-apps",
21
+ selectedAppIds: [" Mail ", "mail", "calendar"],
22
+ updatedAt: "2026-05-20T12:00:00.000Z",
23
+ updatedBy: "admin@example.test",
24
+ }),
25
+ ).toEqual({
26
+ mode: "selected-apps",
27
+ selectedAppIds: ["mail", "calendar"],
28
+ updatedAt: "2026-05-20T12:00:00.000Z",
29
+ updatedBy: "admin@example.test",
30
+ });
31
+ });
32
+ });
33
+
34
+ describe("isAppAllowedByMcpAccess", () => {
35
+ it("allows every app in all-apps mode", () => {
36
+ expect(
37
+ isAppAllowedByMcpAccess("mail", {
38
+ mode: "all-apps",
39
+ selectedAppIds: [],
40
+ }),
41
+ ).toBe(true);
42
+ });
43
+
44
+ it("checks selected grants in selected-apps mode", () => {
45
+ expect(
46
+ isAppAllowedByMcpAccess("mail", {
47
+ mode: "selected-apps",
48
+ selectedAppIds: ["calendar"],
49
+ }),
50
+ ).toBe(false);
51
+ expect(
52
+ isAppAllowedByMcpAccess("calendar", {
53
+ mode: "selected-apps",
54
+ selectedAppIds: ["calendar"],
55
+ }),
56
+ ).toBe(true);
57
+ });
58
+ });
@@ -0,0 +1,104 @@
1
+ import {
2
+ getOrgSetting,
3
+ getUserSetting,
4
+ putOrgSetting,
5
+ putUserSetting,
6
+ } from "@agent-native/core/settings";
7
+ import {
8
+ getRequestOrgId,
9
+ getRequestUserEmail,
10
+ } from "@agent-native/core/server";
11
+
12
+ export const MCP_APP_ACCESS_SETTINGS_KEY = "dispatch-mcp-app-access";
13
+
14
+ export type DispatchMcpAppAccessMode = "all-apps" | "selected-apps";
15
+
16
+ export interface DispatchMcpAppAccessSettings {
17
+ mode: DispatchMcpAppAccessMode;
18
+ selectedAppIds: string[];
19
+ updatedAt?: string;
20
+ updatedBy?: string;
21
+ }
22
+
23
+ interface AccessScope {
24
+ kind: "org" | "user";
25
+ id: string;
26
+ actor: string;
27
+ }
28
+
29
+ function uniqueAppIds(values: unknown): string[] {
30
+ const input = Array.isArray(values) ? values : [];
31
+ return Array.from(
32
+ new Set(
33
+ input
34
+ .filter((value): value is string => typeof value === "string")
35
+ .map((value) => value.trim().toLowerCase())
36
+ .filter(Boolean),
37
+ ),
38
+ );
39
+ }
40
+
41
+ export function normalizeMcpAppAccessSettings(
42
+ raw: unknown,
43
+ ): DispatchMcpAppAccessSettings {
44
+ const record =
45
+ raw && typeof raw === "object" && !Array.isArray(raw)
46
+ ? (raw as Record<string, unknown>)
47
+ : {};
48
+ const mode = record.mode === "selected-apps" ? "selected-apps" : "all-apps";
49
+ return {
50
+ mode,
51
+ selectedAppIds: uniqueAppIds(record.selectedAppIds),
52
+ updatedAt:
53
+ typeof record.updatedAt === "string" ? record.updatedAt : undefined,
54
+ updatedBy:
55
+ typeof record.updatedBy === "string" ? record.updatedBy : undefined,
56
+ };
57
+ }
58
+
59
+ function currentAccessScope(): AccessScope {
60
+ const actor = getRequestUserEmail();
61
+ if (!actor) throw new Error("no authenticated user");
62
+ const orgId = getRequestOrgId();
63
+ if (orgId) return { kind: "org", id: orgId, actor };
64
+ return { kind: "user", id: actor, actor };
65
+ }
66
+
67
+ export async function getDispatchMcpAppAccessSettings(): Promise<DispatchMcpAppAccessSettings> {
68
+ const scope = currentAccessScope();
69
+ const raw =
70
+ scope.kind === "org"
71
+ ? await getOrgSetting(scope.id, MCP_APP_ACCESS_SETTINGS_KEY)
72
+ : await getUserSetting(scope.id, MCP_APP_ACCESS_SETTINGS_KEY);
73
+ return normalizeMcpAppAccessSettings(raw);
74
+ }
75
+
76
+ export async function setDispatchMcpAppAccessSettings(input: {
77
+ mode: DispatchMcpAppAccessMode;
78
+ selectedAppIds?: string[];
79
+ }): Promise<DispatchMcpAppAccessSettings> {
80
+ const scope = currentAccessScope();
81
+ const next: DispatchMcpAppAccessSettings = {
82
+ mode: input.mode,
83
+ selectedAppIds: uniqueAppIds(input.selectedAppIds),
84
+ updatedAt: new Date().toISOString(),
85
+ updatedBy: scope.actor,
86
+ };
87
+ const value = next as unknown as Record<string, unknown>;
88
+ if (scope.kind === "org") {
89
+ await putOrgSetting(scope.id, MCP_APP_ACCESS_SETTINGS_KEY, value);
90
+ } else {
91
+ await putUserSetting(scope.id, MCP_APP_ACCESS_SETTINGS_KEY, value);
92
+ }
93
+ return next;
94
+ }
95
+
96
+ export function isAppAllowedByMcpAccess(
97
+ appId: string,
98
+ settings: DispatchMcpAppAccessSettings,
99
+ ): boolean {
100
+ const normalized = appId.trim().toLowerCase();
101
+ if (!normalized) return false;
102
+ if (settings.mode === "all-apps") return true;
103
+ return settings.selectedAppIds.includes(normalized);
104
+ }