skybridge 0.0.0-dev.f57f202 → 0.0.0-dev.f58ff90

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 (109) 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/use-typescript-check.js +4 -2
  15. package/dist/cli/use-typescript-check.js.map +1 -1
  16. package/dist/server/app.d.ts +22 -5
  17. package/dist/server/app.js +24 -13
  18. package/dist/server/app.js.map +1 -1
  19. package/dist/server/app.test.js +81 -0
  20. package/dist/server/app.test.js.map +1 -1
  21. package/dist/server/events.d.ts +113 -0
  22. package/dist/server/events.js +318 -0
  23. package/dist/server/events.js.map +1 -0
  24. package/dist/server/events.test.d.ts +1 -0
  25. package/dist/server/events.test.js +172 -0
  26. package/dist/server/events.test.js.map +1 -0
  27. package/dist/server/express.d.ts +3 -2
  28. package/dist/server/express.js +17 -11
  29. package/dist/server/express.js.map +1 -1
  30. package/dist/server/express.test.js +45 -0
  31. package/dist/server/express.test.js.map +1 -1
  32. package/dist/server/form-input.d.ts +120 -0
  33. package/dist/server/form-input.js +89 -0
  34. package/dist/server/form-input.js.map +1 -0
  35. package/dist/server/index.d.ts +4 -2
  36. package/dist/server/index.js +3 -1
  37. package/dist/server/index.js.map +1 -1
  38. package/dist/server/mcp-errors.test.js +188 -0
  39. package/dist/server/mcp-errors.test.js.map +1 -1
  40. package/dist/server/metric.d.ts +5 -3
  41. package/dist/server/metric.js +5 -5
  42. package/dist/server/metric.js.map +1 -1
  43. package/dist/server/register-tool.test.js +153 -0
  44. package/dist/server/register-tool.test.js.map +1 -1
  45. package/dist/server/server.d.ts +179 -10
  46. package/dist/server/server.js +247 -23
  47. package/dist/server/server.js.map +1 -1
  48. package/dist/server/templateHelper.d.ts +1 -0
  49. package/dist/server/templateHelper.js.map +1 -1
  50. package/dist/server/templates.generated.js +26 -12
  51. package/dist/server/templates.generated.js.map +1 -1
  52. package/dist/server/tool-extra.test.js +79 -0
  53. package/dist/server/tool-extra.test.js.map +1 -1
  54. package/dist/server/view-resource-resolution.test.js +35 -0
  55. package/dist/server/view-resource-resolution.test.js.map +1 -1
  56. package/dist/web/bridges/adaptor.d.ts +9 -2
  57. package/dist/web/bridges/adaptor.js +113 -10
  58. package/dist/web/bridges/adaptor.js.map +1 -1
  59. package/dist/web/bridges/adaptor.test.js +66 -5
  60. package/dist/web/bridges/adaptor.test.js.map +1 -1
  61. package/dist/web/bridges/apps-sdk/bridge.js +1 -1
  62. package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
  63. package/dist/web/bridges/apps-sdk/types.d.ts +3 -4
  64. package/dist/web/bridges/apps-sdk/types.js.map +1 -1
  65. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.d.ts +3 -3
  66. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js +3 -3
  67. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js.map +1 -1
  68. package/dist/web/bridges/mcp-app/types.d.ts +5 -13
  69. package/dist/web/bridges/mcp-app/types.js.map +1 -1
  70. package/dist/web/bridges/mcp-app/use-mcp-app-context.d.ts +1 -1
  71. package/dist/web/bridges/mcp-app/use-mcp-app-context.js +1 -1
  72. package/dist/web/bridges/mcp-app/use-mcp-app-context.js.map +1 -1
  73. package/dist/web/bridges/types.d.ts +45 -6
  74. package/dist/web/bridges/types.js +1 -1
  75. package/dist/web/bridges/types.js.map +1 -1
  76. package/dist/web/hooks/index.d.ts +2 -0
  77. package/dist/web/hooks/index.js +2 -0
  78. package/dist/web/hooks/index.js.map +1 -1
  79. package/dist/web/hooks/openai-extensions.test.js +1 -8
  80. package/dist/web/hooks/openai-extensions.test.js.map +1 -1
  81. package/dist/web/hooks/use-call-tool.js +1 -1
  82. package/dist/web/hooks/use-call-tool.js.map +1 -1
  83. package/dist/web/hooks/use-call-tool.test.js +6 -2
  84. package/dist/web/hooks/use-call-tool.test.js.map +1 -1
  85. package/dist/web/hooks/use-deep-link.js +1 -27
  86. package/dist/web/hooks/use-deep-link.js.map +1 -1
  87. package/dist/web/hooks/use-file-resource.d.ts +37 -0
  88. package/dist/web/hooks/use-file-resource.js +98 -0
  89. package/dist/web/hooks/use-file-resource.js.map +1 -0
  90. package/dist/web/hooks/use-file-resource.test.d.ts +1 -0
  91. package/dist/web/hooks/use-file-resource.test.js +65 -0
  92. package/dist/web/hooks/use-file-resource.test.js.map +1 -0
  93. package/dist/web/hooks/use-host.d.ts +3 -3
  94. package/dist/web/hooks/use-host.js +4 -4
  95. package/dist/web/hooks/use-host.js.map +1 -1
  96. package/dist/web/hooks/use-model-context.js +3 -5
  97. package/dist/web/hooks/use-model-context.js.map +1 -1
  98. package/dist/web/hooks/use-open-file.d.ts +20 -0
  99. package/dist/web/hooks/use-open-file.js +25 -0
  100. package/dist/web/hooks/use-open-file.js.map +1 -0
  101. package/dist/web/hooks/use-register-view-tool.d.ts +1 -1
  102. package/dist/web/hooks/use-register-view-tool.js +1 -1
  103. package/dist/web/hooks/use-register-view-tool.js.map +1 -1
  104. package/dist/web/hooks/use-send-follow-up-message.d.ts +8 -2
  105. package/dist/web/hooks/use-send-follow-up-message.js +6 -0
  106. package/dist/web/hooks/use-send-follow-up-message.js.map +1 -1
  107. package/dist/web/mount-view.js +3 -5
  108. package/dist/web/mount-view.js.map +1 -1
  109. package/package.json +4 -2
@@ -1,17 +1,22 @@
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;
13
- function definedEntries(values) {
14
- return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
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
+ }
15
20
  }
16
21
  function acceptsEmptyInput(inputSchema) {
17
22
  if (inputSchema === undefined) {
@@ -36,6 +41,12 @@ function normalizeForwardedPrefix(raw) {
36
41
  }
37
42
  return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
38
43
  }
44
+ const fileEntrypointInput = {
45
+ file: z.object({
46
+ name: z.string().min(1),
47
+ resourceUri: z.string().trim().min(1),
48
+ }),
49
+ };
39
50
  /**
40
51
  * Drop the query string from a `ui://` view URI, leaving the bare path. The
41
52
  * `?v=` cache key is the only query we append, so a plain split is enough and
@@ -150,6 +161,7 @@ export class McpServer extends McpServerBaseOmitted {
150
161
  resolveResourceMetadataUrl;
151
162
  toolSecuritySchemes = new Map();
152
163
  userMiddlewareEntries = [];
164
+ eventRegistry;
153
165
  constructor(serverInfo, options, skybridgeOptions) {
154
166
  super(serverInfo, withSkillsCapability(options, skybridgeOptions));
155
167
  this.oauthEnabled = Boolean(skybridgeOptions?.oauth);
@@ -343,7 +355,7 @@ export class McpServer extends McpServerBaseOmitted {
343
355
  }
344
356
  return { serverUrl, assetsBasePath, connectDomains, contentMetaOverrides };
345
357
  }
346
- registerViewResources(toolName, view, toolMeta, openaiResourceMeta) {
358
+ registerViewResources(toolName, view, toolMeta, openai) {
347
359
  // Append a content-derived version param so hosts (e.g. ChatGPT) bust
348
360
  // their cache when the bundle changes, but keep the URI stable across
349
361
  // `tools/list` calls when the bundle hasn't changed.
@@ -379,12 +391,10 @@ export class McpServer extends McpServerBaseOmitted {
379
391
  }),
380
392
  };
381
393
  const meta = { ...base, ...view._meta };
382
- if (openaiResourceMeta && Object.keys(openaiResourceMeta).length > 0) {
383
- meta["openai/ui"] = {
384
- ...view._meta?.["openai/ui"],
385
- ...openaiResourceMeta,
386
- };
387
- }
394
+ mergeOpenAIUi(meta, {
395
+ availableDisplayModes: openai?.availableDisplayModes,
396
+ preferredDisplayMode: openai?.preferredDisplayMode,
397
+ });
388
398
  return meta;
389
399
  },
390
400
  };
@@ -423,6 +433,7 @@ export class McpServer extends McpServerBaseOmitted {
423
433
  serverUrl: viewBase,
424
434
  viewFile: this.lookupViewFile(view.component),
425
435
  styleFile: this.lookupDistFile("style.css") ?? "",
436
+ preloadFiles: this.lookupViewPreloadFiles(view.component),
426
437
  })
427
438
  : templateHelper.renderDevelopment({
428
439
  serverUrl: viewBase,
@@ -457,6 +468,9 @@ export class McpServer extends McpServerBaseOmitted {
457
468
  captureToolError(extra, error);
458
469
  throw error;
459
470
  }
471
+ if (isInputRequiredResult(result)) {
472
+ return result;
473
+ }
460
474
  warnOnLargeToolOutput(result, toolName);
461
475
  return {
462
476
  ...result,
@@ -499,6 +513,21 @@ export class McpServer extends McpServerBaseOmitted {
499
513
  }
500
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.`);
501
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
+ }
502
531
  lookupDistFile(key) {
503
532
  const manifest = this.readManifest();
504
533
  return manifest[key]?.file;
@@ -520,6 +549,211 @@ export class McpServer extends McpServerBaseOmitted {
520
549
  cachedDiskManifest ??= JSON.parse(readFileSync(path.join(process.cwd(), "dist", "assets", ".vite", "manifest.json"), "utf-8"));
521
550
  return cachedDiskManifest ?? {};
522
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
+ }
523
757
  registerTool(rawConfig, rawCb) {
524
758
  const baseFn = McpServerBase.prototype.registerTool;
525
759
  const config = rawConfig;
@@ -541,7 +775,7 @@ export class McpServer extends McpServerBaseOmitted {
541
775
  // descriptor, but the SDK's `registerTool` drops unknown top-level
542
776
  // fields, so the canonical spot isn't reachable without intercepting
543
777
  // `tools/list`. Use the `_meta` back-compat mirror documented in the
544
- // Apps SDK reference until SEP-1488 lands in the spec.
778
+ // ChatGPT plugin reference until SEP-1488 lands in the spec.
545
779
  toolMeta.securitySchemes = securitySchemes;
546
780
  }
547
781
  if (openai) {
@@ -558,24 +792,14 @@ export class McpServer extends McpServerBaseOmitted {
558
792
  if (badExtension !== undefined) {
559
793
  throw new Error(`skybridge: tool "${name}" has a file entrypoint extension "${badExtension}" that doesn't start with ".".`);
560
794
  }
561
- const openaiUi = definedEntries({
795
+ mergeOpenAIUi(toolMeta, {
562
796
  entrypoints: openai.entrypoints,
563
797
  preferredModelDisplayMode: openai.preferredModelDisplayMode,
564
798
  });
565
- if (Object.keys(openaiUi).length > 0) {
566
- toolMeta["openai/ui"] = {
567
- ...userToolMeta?.["openai/ui"],
568
- ...openaiUi,
569
- };
570
- }
571
799
  }
572
800
  if (view) {
573
801
  this.enforceOneToolPerView(view.component, name);
574
- this.registerViewResources(name, view, toolMeta, openai &&
575
- definedEntries({
576
- availableDisplayModes: openai.availableDisplayModes,
577
- preferredDisplayMode: openai.preferredDisplayMode,
578
- }));
802
+ this.registerViewResources(name, view, toolMeta, openai);
579
803
  }
580
804
  const wrappedCb = this.decorateToolHandler(cb, {
581
805
  attachViewUUID: Boolean(view),