skybridge 2.0.4 → 2.1.1

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 (139) hide show
  1. package/README.md +9 -9
  2. package/dist/cli/build-helpers.d.ts +1 -1
  3. package/dist/cli/build-helpers.js +9 -1
  4. package/dist/cli/build-helpers.js.map +1 -1
  5. package/dist/cli/build-helpers.test.js +24 -0
  6. package/dist/cli/build-helpers.test.js.map +1 -1
  7. package/dist/cli/build-steps.js +16 -5
  8. package/dist/cli/build-steps.js.map +1 -1
  9. package/dist/cli/build-steps.test.js +20 -0
  10. package/dist/cli/build-steps.test.js.map +1 -1
  11. package/dist/cli/resolve-skybridge-config.d.ts +1 -0
  12. package/dist/cli/resolve-skybridge-config.js +11 -1
  13. package/dist/cli/resolve-skybridge-config.js.map +1 -1
  14. package/dist/cli/run-plain.js +2 -1
  15. package/dist/cli/run-plain.js.map +1 -1
  16. package/dist/cli/use-nodemon.d.ts +11 -1
  17. package/dist/cli/use-nodemon.js +19 -5
  18. package/dist/cli/use-nodemon.js.map +1 -1
  19. package/dist/cli/use-typescript-check.js +4 -2
  20. package/dist/cli/use-typescript-check.js.map +1 -1
  21. package/dist/commands/dev.js +3 -3
  22. package/dist/commands/dev.js.map +1 -1
  23. package/dist/server/app.d.ts +22 -5
  24. package/dist/server/app.js +24 -13
  25. package/dist/server/app.js.map +1 -1
  26. package/dist/server/app.test.js +81 -0
  27. package/dist/server/app.test.js.map +1 -1
  28. package/dist/server/auth/providers/custom.d.ts +4 -2
  29. package/dist/server/auth/providers/custom.js.map +1 -1
  30. package/dist/server/auth/providers/workos.d.ts +3 -2
  31. package/dist/server/auth/providers/workos.js +2 -1
  32. package/dist/server/auth/providers/workos.js.map +1 -1
  33. package/dist/server/auth/verify.d.ts +5 -3
  34. package/dist/server/auth/verify.js.map +1 -1
  35. package/dist/server/auth/verify.test.js +16 -0
  36. package/dist/server/auth/verify.test.js.map +1 -1
  37. package/dist/server/events.d.ts +113 -0
  38. package/dist/server/events.js +318 -0
  39. package/dist/server/events.js.map +1 -0
  40. package/dist/server/events.test.d.ts +1 -0
  41. package/dist/server/events.test.js +172 -0
  42. package/dist/server/events.test.js.map +1 -0
  43. package/dist/server/express.d.ts +3 -2
  44. package/dist/server/express.js +17 -11
  45. package/dist/server/express.js.map +1 -1
  46. package/dist/server/express.test.js +45 -0
  47. package/dist/server/express.test.js.map +1 -1
  48. package/dist/server/form-input.d.ts +120 -0
  49. package/dist/server/form-input.js +89 -0
  50. package/dist/server/form-input.js.map +1 -0
  51. package/dist/server/index.d.ts +4 -2
  52. package/dist/server/index.js +3 -1
  53. package/dist/server/index.js.map +1 -1
  54. package/dist/server/mcp-errors.test.js +188 -0
  55. package/dist/server/mcp-errors.test.js.map +1 -1
  56. package/dist/server/metric.d.ts +5 -3
  57. package/dist/server/metric.js +5 -5
  58. package/dist/server/metric.js.map +1 -1
  59. package/dist/server/register-tool.test.js +203 -0
  60. package/dist/server/register-tool.test.js.map +1 -1
  61. package/dist/server/server.d.ts +255 -10
  62. package/dist/server/server.js +278 -9
  63. package/dist/server/server.js.map +1 -1
  64. package/dist/server/templateHelper.d.ts +1 -0
  65. package/dist/server/templateHelper.js.map +1 -1
  66. package/dist/server/templates.generated.js +26 -12
  67. package/dist/server/templates.generated.js.map +1 -1
  68. package/dist/server/tool-extra.test.js +79 -0
  69. package/dist/server/tool-extra.test.js.map +1 -1
  70. package/dist/server/view-resource-resolution.test.js +35 -0
  71. package/dist/server/view-resource-resolution.test.js.map +1 -1
  72. package/dist/web/bridges/adaptor.d.ts +14 -3
  73. package/dist/web/bridges/adaptor.js +164 -14
  74. package/dist/web/bridges/adaptor.js.map +1 -1
  75. package/dist/web/bridges/adaptor.test.js +62 -6
  76. package/dist/web/bridges/adaptor.test.js.map +1 -1
  77. package/dist/web/bridges/apps-sdk/bridge.js +1 -1
  78. package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
  79. package/dist/web/bridges/apps-sdk/types.d.ts +2 -3
  80. package/dist/web/bridges/apps-sdk/types.js.map +1 -1
  81. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.d.ts +3 -3
  82. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js +3 -3
  83. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js.map +1 -1
  84. package/dist/web/bridges/mcp-app/bridge.d.ts +1 -1
  85. package/dist/web/bridges/mcp-app/bridge.js +9 -2
  86. package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
  87. package/dist/web/bridges/mcp-app/types.d.ts +18 -3
  88. package/dist/web/bridges/mcp-app/types.js.map +1 -1
  89. package/dist/web/bridges/mcp-app/use-mcp-app-context.d.ts +2 -2
  90. package/dist/web/bridges/mcp-app/use-mcp-app-context.js +1 -1
  91. package/dist/web/bridges/mcp-app/use-mcp-app-context.js.map +1 -1
  92. package/dist/web/bridges/types.d.ts +57 -4
  93. package/dist/web/bridges/types.js +1 -1
  94. package/dist/web/bridges/types.js.map +1 -1
  95. package/dist/web/data-llm.js +3 -2
  96. package/dist/web/data-llm.js.map +1 -1
  97. package/dist/web/data-llm.test.js +6 -0
  98. package/dist/web/data-llm.test.js.map +1 -1
  99. package/dist/web/generate-helpers.d.ts +2 -2
  100. package/dist/web/generate-helpers.js +2 -2
  101. package/dist/web/generate-helpers.js.map +1 -1
  102. package/dist/web/hooks/index.d.ts +5 -1
  103. package/dist/web/hooks/index.js +5 -1
  104. package/dist/web/hooks/index.js.map +1 -1
  105. package/dist/web/hooks/openai-extensions.test.d.ts +1 -0
  106. package/dist/web/hooks/openai-extensions.test.js +107 -0
  107. package/dist/web/hooks/openai-extensions.test.js.map +1 -0
  108. package/dist/web/hooks/use-call-tool.d.ts +2 -2
  109. package/dist/web/hooks/use-call-tool.js +15 -10
  110. package/dist/web/hooks/use-call-tool.js.map +1 -1
  111. package/dist/web/hooks/use-call-tool.test.js +13 -2
  112. package/dist/web/hooks/use-call-tool.test.js.map +1 -1
  113. package/dist/web/hooks/use-deep-link.d.ts +22 -0
  114. package/dist/web/hooks/use-deep-link.js +34 -0
  115. package/dist/web/hooks/use-deep-link.js.map +1 -0
  116. package/dist/web/hooks/use-file-resource.d.ts +37 -0
  117. package/dist/web/hooks/use-file-resource.js +98 -0
  118. package/dist/web/hooks/use-file-resource.js.map +1 -0
  119. package/dist/web/hooks/use-file-resource.test.d.ts +1 -0
  120. package/dist/web/hooks/use-file-resource.test.js +65 -0
  121. package/dist/web/hooks/use-file-resource.test.js.map +1 -0
  122. package/dist/web/hooks/use-host.d.ts +27 -5
  123. package/dist/web/hooks/use-host.js +20 -5
  124. package/dist/web/hooks/use-host.js.map +1 -1
  125. package/dist/web/hooks/use-model-context.d.ts +44 -0
  126. package/dist/web/hooks/use-model-context.js +63 -0
  127. package/dist/web/hooks/use-model-context.js.map +1 -0
  128. package/dist/web/hooks/use-open-file.d.ts +20 -0
  129. package/dist/web/hooks/use-open-file.js +25 -0
  130. package/dist/web/hooks/use-open-file.js.map +1 -0
  131. package/dist/web/hooks/use-register-view-tool.d.ts +1 -1
  132. package/dist/web/hooks/use-register-view-tool.js +1 -1
  133. package/dist/web/hooks/use-register-view-tool.js.map +1 -1
  134. package/dist/web/hooks/use-send-follow-up-message.d.ts +8 -2
  135. package/dist/web/hooks/use-send-follow-up-message.js +6 -0
  136. package/dist/web/hooks/use-send-follow-up-message.js.map +1 -1
  137. package/dist/web/mount-view.js +3 -5
  138. package/dist/web/mount-view.js.map +1 -1
  139. package/package.json +6 -5
@@ -1,15 +1,32 @@
1
1
  import crypto from "node:crypto";
2
2
  import { readFileSync } from "node:fs";
3
3
  import path from "node:path";
4
- import { McpServer as McpServerBase, } from "@modelcontextprotocol/server";
4
+ import { isInputRequiredResult, McpServer as McpServerBase, } from "@modelcontextprotocol/server";
5
+ import { z } from "zod/v4";
5
6
  import { warnOnLargeToolOutput } from "../context-warnings.js";
6
7
  import { authToSecuritySchemes, evaluateSecuritySchemes, inBandChallengeResult, } from "./auth/security-schemes.js";
8
+ import { EventRegistry, } from "./events.js";
7
9
  import { hostFromUserAgent } from "./host.js";
8
10
  import { captureToolError } from "./middleware.js";
9
11
  import { resolveServerOrigin } from "./requestOrigin.js";
10
12
  import { discoverSkills, registerSkills, SKILLS_EXTENSION_KEY, } from "./skills.js";
11
13
  import { templateHelper } from "./templateHelper.js";
12
14
  const unionOf = (base, extra) => extra ? [...new Set([...base, ...extra])] : base;
15
+ function mergeOpenAIUi(meta, values) {
16
+ const defined = Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
17
+ if (Object.keys(defined).length > 0) {
18
+ meta["openai/ui"] = { ...meta["openai/ui"], ...defined };
19
+ }
20
+ }
21
+ function acceptsEmptyInput(inputSchema) {
22
+ if (inputSchema === undefined) {
23
+ return true;
24
+ }
25
+ const checks = "~standard" in inputSchema
26
+ ? [inputSchema["~standard"].validate({})]
27
+ : Object.values(inputSchema).map((schema) => schema["~standard"].validate(undefined));
28
+ return checks.every((result) => result instanceof Promise || result.issues === undefined);
29
+ }
13
30
  const SKILLS_DIR = "src/skills";
14
31
  /**
15
32
  * Normalize an `x-forwarded-prefix` value into a leading-slash, no-trailing-slash
@@ -24,6 +41,12 @@ function normalizeForwardedPrefix(raw) {
24
41
  }
25
42
  return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
26
43
  }
44
+ const fileEntrypointInput = {
45
+ file: z.object({
46
+ name: z.string().min(1),
47
+ resourceUri: z.string().trim().min(1),
48
+ }),
49
+ };
27
50
  /**
28
51
  * Drop the query string from a `ui://` view URI, leaving the bare path. The
29
52
  * `?v=` cache key is the only query we append, so a plain split is enough and
@@ -138,6 +161,7 @@ export class McpServer extends McpServerBaseOmitted {
138
161
  resolveResourceMetadataUrl;
139
162
  toolSecuritySchemes = new Map();
140
163
  userMiddlewareEntries = [];
164
+ eventRegistry;
141
165
  constructor(serverInfo, options, skybridgeOptions) {
142
166
  super(serverInfo, withSkillsCapability(options, skybridgeOptions));
143
167
  this.oauthEnabled = Boolean(skybridgeOptions?.oauth);
@@ -331,7 +355,7 @@ export class McpServer extends McpServerBaseOmitted {
331
355
  }
332
356
  return { serverUrl, assetsBasePath, connectDomains, contentMetaOverrides };
333
357
  }
334
- registerViewResources(toolName, view, toolMeta) {
358
+ registerViewResources(toolName, view, toolMeta, openai) {
335
359
  // Append a content-derived version param so hosts (e.g. ChatGPT) bust
336
360
  // their cache when the bundle changes, but keep the URI stable across
337
361
  // `tools/list` calls when the bundle hasn't changed.
@@ -366,10 +390,12 @@ export class McpServer extends McpServerBaseOmitted {
366
390
  "openai/widgetCSP": { redirect_domains: view.csp.redirectDomains },
367
391
  }),
368
392
  };
369
- if (view._meta) {
370
- return { ...base, ...view._meta };
371
- }
372
- return base;
393
+ const meta = { ...base, ...view._meta };
394
+ mergeOpenAIUi(meta, {
395
+ availableDisplayModes: openai?.availableDisplayModes,
396
+ preferredDisplayMode: openai?.preferredDisplayMode,
397
+ });
398
+ return meta;
373
399
  },
374
400
  };
375
401
  this.registerViewResource({ name: toolName, viewResource, view });
@@ -407,6 +433,7 @@ export class McpServer extends McpServerBaseOmitted {
407
433
  serverUrl: viewBase,
408
434
  viewFile: this.lookupViewFile(view.component),
409
435
  styleFile: this.lookupDistFile("style.css") ?? "",
436
+ preloadFiles: this.lookupViewPreloadFiles(view.component),
410
437
  })
411
438
  : templateHelper.renderDevelopment({
412
439
  serverUrl: viewBase,
@@ -441,6 +468,9 @@ export class McpServer extends McpServerBaseOmitted {
441
468
  captureToolError(extra, error);
442
469
  throw error;
443
470
  }
471
+ if (isInputRequiredResult(result)) {
472
+ return result;
473
+ }
444
474
  warnOnLargeToolOutput(result, toolName);
445
475
  return {
446
476
  ...result,
@@ -483,6 +513,21 @@ export class McpServer extends McpServerBaseOmitted {
483
513
  }
484
514
  throw new Error(`View "${viewName}" not found in Vite manifest. Did the build complete successfully? Look for an entry with name "${viewName}" in dist/assets/.vite/manifest.json.`);
485
515
  }
516
+ lookupViewPreloadFiles(viewName) {
517
+ const manifest = this.readManifest();
518
+ const entryKey = Object.keys(manifest).find((key) => manifest[key]?.isEntry && manifest[key]?.name === viewName);
519
+ const files = new Set();
520
+ const visit = (key) => {
521
+ const chunk = key ? manifest[key] : undefined;
522
+ if (!chunk || files.has(chunk.file)) {
523
+ return;
524
+ }
525
+ files.add(chunk.file);
526
+ chunk.imports?.forEach(visit);
527
+ };
528
+ visit(entryKey);
529
+ return [...files];
530
+ }
486
531
  lookupDistFile(key) {
487
532
  const manifest = this.readManifest();
488
533
  return manifest[key]?.file;
@@ -504,11 +549,216 @@ export class McpServer extends McpServerBaseOmitted {
504
549
  cachedDiskManifest ??= JSON.parse(readFileSync(path.join(process.cwd(), "dist", "assets", ".vite", "manifest.json"), "utf-8"));
505
550
  return cachedDiskManifest ?? {};
506
551
  }
552
+ /**
553
+ * @experimental Register an MCP Events type that hosts can subscribe to, per
554
+ * the Triggers & Events working group draft. Skybridge serves `events/list`,
555
+ * `events/subscribe` and `events/unsubscribe`, checks auth and arguments,
556
+ * verifies the callback URL, then hands each subscription to `onSubscribe`.
557
+ * Delivering the events is up to you, for example with {@link deliverEvent}.
558
+ * API may change.
559
+ *
560
+ * @example
561
+ * ```ts
562
+ * server.registerEvent(
563
+ * {
564
+ * name: "comment.created",
565
+ * description: "Fires when someone comments on a document",
566
+ * inputSchema: { documentId: z.string() },
567
+ * payloadSchema: { documentId: z.string(), excerpt: z.string() },
568
+ * },
569
+ * {
570
+ * onSubscribe: ({ documentId }, { subscription }) =>
571
+ * webhooks.upsert({ ...subscription, filter: { documentId } }),
572
+ * onUnsubscribe: (_args, { subscription }) =>
573
+ * webhooks.remove(subscription.id),
574
+ * },
575
+ * );
576
+ * ```
577
+ *
578
+ * @see https://docs.skybridge.tech/api-reference/register-event
579
+ */
580
+ registerEvent(config, hooks) {
581
+ if (!this.eventRegistry) {
582
+ this.server.registerCapabilities({ events: {} });
583
+ this.eventRegistry = new EventRegistry(this.server);
584
+ }
585
+ this.eventRegistry.add(config, hooks);
586
+ return this;
587
+ }
588
+ /**
589
+ * Register a ChatGPT file viewer, from the OpenAI MCP extensions: a tool
590
+ * with a file entrypoint for `extensions` and a view. ChatGPT desktop calls
591
+ * it with the file the user opened, as `{ file: { name, resourceUri } }`.
592
+ * Read and save the file from the view with `useFileResource`.
593
+ *
594
+ * @example
595
+ * ```ts
596
+ * server.registerFileViewer(
597
+ * {
598
+ * name: "part-viewer",
599
+ * title: "Part Viewer",
600
+ * extensions: [".stl", ".step"],
601
+ * view: { component: "part-viewer" },
602
+ * },
603
+ * async ({ file }) => ({ structuredContent: { name: file.name } }),
604
+ * );
605
+ * ```
606
+ *
607
+ * @see https://docs.skybridge.tech/api-reference/register-file-viewer
608
+ */
609
+ registerFileViewer(config, cb) {
610
+ const { extensions, openai, ...toolConfig } = config;
611
+ return this.registerTool({
612
+ ...toolConfig,
613
+ inputSchema: fileEntrypointInput,
614
+ openai: { ...openai, entrypoints: [{ type: "file", extensions }] },
615
+ }, cb);
616
+ }
617
+ /**
618
+ * Register the composer at-mention search for ChatGPT, from the OpenAI MCP
619
+ * extensions. When the user types `@` and your app name, ChatGPT calls
620
+ * `handler` with the typed text and lists the returned resource links.
621
+ * Skybridge registers it as a tool hidden from the model.
622
+ *
623
+ * @example
624
+ * ```ts
625
+ * server.registerMentions({
626
+ * name: "search_parts",
627
+ * handler: async ({ query }) => ({
628
+ * items: findParts(query).map((part) => ({
629
+ * type: "resource_link",
630
+ * uri: `parts://${part.id}`,
631
+ * name: part.name,
632
+ * })),
633
+ * }),
634
+ * });
635
+ * ```
636
+ *
637
+ * @see https://docs.skybridge.tech/api-reference/register-mentions
638
+ */
639
+ registerMentions(config) {
640
+ this.registerTool({
641
+ name: config.name,
642
+ description: config.description ?? "Search items to mention.",
643
+ annotations: { readOnlyHint: true },
644
+ inputSchema: { query: z.string() },
645
+ _meta: {
646
+ "openai/extensions": { "mentions/search": {} },
647
+ ui: { visibility: ["app"] },
648
+ },
649
+ }, async ({ query }, extra) => ({
650
+ content: [],
651
+ structuredContent: await config.handler({ query }, extra),
652
+ }));
653
+ return this;
654
+ }
655
+ /**
656
+ * Register app settings for ChatGPT, from the OpenAI MCP extensions.
657
+ * Skybridge registers a read tool and an update tool from `fields`, and
658
+ * advertises them in the `openai/settings` server capability. ChatGPT shows
659
+ * the settings page and calls `update` with the changed fields only. Store
660
+ * the values yourself, for example per user.
661
+ *
662
+ * @example
663
+ * ```ts
664
+ * server.registerSettings({
665
+ * fields: {
666
+ * units: { title: "Units", schema: z.enum(["mm", "in"]) },
667
+ * showGrid: { title: "Show grid", schema: z.boolean() },
668
+ * },
669
+ * read: (extra) => loadSettings(extra),
670
+ * update: (set, extra) => saveSettings(set, extra),
671
+ * });
672
+ * ```
673
+ *
674
+ * @see https://docs.skybridge.tech/api-reference/register-settings
675
+ */
676
+ registerSettings(config) {
677
+ const { fields, layout, readTool = "settings.read", updateTool = "settings.update", } = config;
678
+ const values = z
679
+ .strictObject(Object.fromEntries(Object.entries(fields).map(([name, { schema, title, description }]) => [
680
+ name,
681
+ schema.meta({
682
+ title,
683
+ ...(description !== undefined && { description }),
684
+ }),
685
+ ])))
686
+ .required();
687
+ const { $schema: _, ...schema } = z.toJSONSchema(values);
688
+ for (const [name, property] of Object.entries(schema.properties ?? {})) {
689
+ if (fields[name]?.schema.safeParse(undefined).data !== undefined) {
690
+ throw new Error(`Setting "${name}" declares a default: return current values from \`read\` instead.`);
691
+ }
692
+ if (typeof property === "boolean" ||
693
+ !["boolean", "string", "number", "integer"].includes(String(property.type)) ||
694
+ (property.enum !== undefined && property.type !== "string")) {
695
+ throw new Error(`Setting "${name}" must be a boolean, string, string enum, number or integer.`);
696
+ }
697
+ }
698
+ const placed = new Set();
699
+ for (const group of layout ?? []) {
700
+ for (const item of group.items) {
701
+ if (item.kind !== "property") {
702
+ continue;
703
+ }
704
+ if (!Object.hasOwn(fields, item.property) ||
705
+ placed.has(item.property)) {
706
+ throw new Error(`Settings layout names an unknown or duplicate field "${item.property}".`);
707
+ }
708
+ placed.add(item.property);
709
+ }
710
+ }
711
+ this.registerTool({
712
+ name: readTool,
713
+ description: "Read the app settings.",
714
+ annotations: { readOnlyHint: true, openWorldHint: false },
715
+ inputSchema: {},
716
+ outputSchema: {
717
+ schema: z.record(z.string(), z.unknown()),
718
+ values,
719
+ layout: z.array(z.record(z.string(), z.unknown())).optional(),
720
+ },
721
+ }, async (_args, extra) => ({
722
+ content: [],
723
+ structuredContent: {
724
+ schema,
725
+ values: values.parse(await config.read(extra)),
726
+ layout,
727
+ },
728
+ }));
729
+ this.registerTool({
730
+ name: updateTool,
731
+ description: "Update the app settings.",
732
+ annotations: {
733
+ destructiveHint: false,
734
+ idempotentHint: true,
735
+ openWorldHint: false,
736
+ },
737
+ inputSchema: {
738
+ set: values
739
+ .partial()
740
+ .refine((set) => Object.keys(set).length > 0)
741
+ .meta({ minProperties: 1 }),
742
+ },
743
+ outputSchema: { values },
744
+ }, async ({ set }, extra) => ({
745
+ content: [],
746
+ structuredContent: {
747
+ values: values.parse(await config.update(set, extra)),
748
+ },
749
+ }));
750
+ const capability = { readTool, updateTool };
751
+ this.server.registerCapabilities({
752
+ extensions: { "openai/settings": capability },
753
+ experimental: { "openai/settings": capability },
754
+ });
755
+ return this;
756
+ }
507
757
  registerTool(rawConfig, rawCb) {
508
758
  const baseFn = McpServerBase.prototype.registerTool;
509
759
  const config = rawConfig;
510
760
  const cb = rawCb;
511
- const { name, view, auth, securitySchemes: rawSecuritySchemes, _meta: userToolMeta, ...toolFields } = config;
761
+ const { name, view, auth, securitySchemes: rawSecuritySchemes, openai, _meta: userToolMeta, ...toolFields } = config;
512
762
  const authNeedsProvider = auth !== undefined &&
513
763
  (!auth.allowsAnonymous || Boolean(auth.scopes?.length));
514
764
  if (rawSecuritySchemes === undefined &&
@@ -525,12 +775,31 @@ export class McpServer extends McpServerBaseOmitted {
525
775
  // descriptor, but the SDK's `registerTool` drops unknown top-level
526
776
  // fields, so the canonical spot isn't reachable without intercepting
527
777
  // `tools/list`. Use the `_meta` back-compat mirror documented in the
528
- // Apps SDK reference until SEP-1488 lands in the spec.
778
+ // ChatGPT plugin reference until SEP-1488 lands in the spec.
529
779
  toolMeta.securitySchemes = securitySchemes;
530
780
  }
781
+ if (openai) {
782
+ if (!view) {
783
+ throw new Error(`skybridge: tool "${name}" sets \`openai\` options but has no \`view\`.`);
784
+ }
785
+ const opensWithoutInput = openai.entrypoints?.some(({ type }) => type === "global" || type === "thread");
786
+ if (opensWithoutInput && !acceptsEmptyInput(toolFields.inputSchema)) {
787
+ throw new Error(`skybridge: tool "${name}" has a global or thread entrypoint, so ChatGPT calls it with \`{}\`, but its input schema rejects \`{}\`. Make every input optional.`);
788
+ }
789
+ const badExtension = openai.entrypoints
790
+ ?.flatMap((entrypoint) => entrypoint.type === "file" ? entrypoint.extensions : [])
791
+ .find((extension) => !extension.startsWith("."));
792
+ if (badExtension !== undefined) {
793
+ throw new Error(`skybridge: tool "${name}" has a file entrypoint extension "${badExtension}" that doesn't start with ".".`);
794
+ }
795
+ mergeOpenAIUi(toolMeta, {
796
+ entrypoints: openai.entrypoints,
797
+ preferredModelDisplayMode: openai.preferredModelDisplayMode,
798
+ });
799
+ }
531
800
  if (view) {
532
801
  this.enforceOneToolPerView(view.component, name);
533
- this.registerViewResources(name, view, toolMeta);
802
+ this.registerViewResources(name, view, toolMeta, openai);
534
803
  }
535
804
  const wrappedCb = this.decorateToolHandler(cb, {
536
805
  attachViewUUID: Boolean(view),