skybridge 2.0.3 → 2.1.0
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.
- package/README.md +9 -9
- package/dist/cli/run-plain.js +2 -1
- package/dist/cli/run-plain.js.map +1 -1
- package/dist/cli/use-nodemon.d.ts +11 -1
- package/dist/cli/use-nodemon.js +19 -5
- package/dist/cli/use-nodemon.js.map +1 -1
- package/dist/commands/dev.js +3 -3
- package/dist/commands/dev.js.map +1 -1
- package/dist/server/app.d.ts +22 -5
- package/dist/server/app.js +24 -13
- package/dist/server/app.js.map +1 -1
- package/dist/server/app.test.js +81 -0
- package/dist/server/app.test.js.map +1 -1
- package/dist/server/auth/providers/custom.d.ts +4 -2
- package/dist/server/auth/providers/custom.js.map +1 -1
- package/dist/server/auth/providers/workos.d.ts +3 -2
- package/dist/server/auth/providers/workos.js +2 -1
- package/dist/server/auth/providers/workos.js.map +1 -1
- package/dist/server/auth/verify.d.ts +5 -3
- package/dist/server/auth/verify.js.map +1 -1
- package/dist/server/auth/verify.test.js +16 -0
- package/dist/server/auth/verify.test.js.map +1 -1
- package/dist/server/events.d.ts +113 -0
- package/dist/server/events.js +318 -0
- package/dist/server/events.js.map +1 -0
- package/dist/server/events.test.d.ts +1 -0
- package/dist/server/events.test.js +172 -0
- package/dist/server/events.test.js.map +1 -0
- package/dist/server/express.d.ts +3 -2
- package/dist/server/express.js +17 -11
- package/dist/server/express.js.map +1 -1
- package/dist/server/express.test.js +45 -0
- package/dist/server/express.test.js.map +1 -1
- package/dist/server/form-input.d.ts +120 -0
- package/dist/server/form-input.js +89 -0
- package/dist/server/form-input.js.map +1 -0
- package/dist/server/index.d.ts +4 -2
- package/dist/server/index.js +3 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp-errors.test.js +188 -0
- package/dist/server/mcp-errors.test.js.map +1 -1
- package/dist/server/metric.d.ts +5 -3
- package/dist/server/metric.js +5 -5
- package/dist/server/metric.js.map +1 -1
- package/dist/server/register-tool.test.js +203 -0
- package/dist/server/register-tool.test.js.map +1 -1
- package/dist/server/server.d.ts +254 -10
- package/dist/server/server.js +262 -9
- package/dist/server/server.js.map +1 -1
- package/dist/server/tool-extra.test.js +79 -0
- package/dist/server/tool-extra.test.js.map +1 -1
- package/dist/web/bridges/adaptor.d.ts +14 -3
- package/dist/web/bridges/adaptor.js +164 -14
- package/dist/web/bridges/adaptor.js.map +1 -1
- package/dist/web/bridges/adaptor.test.js +62 -6
- package/dist/web/bridges/adaptor.test.js.map +1 -1
- package/dist/web/bridges/apps-sdk/bridge.js +1 -1
- package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
- package/dist/web/bridges/apps-sdk/types.d.ts +2 -3
- package/dist/web/bridges/apps-sdk/types.js.map +1 -1
- package/dist/web/bridges/apps-sdk/use-apps-sdk-context.d.ts +3 -3
- package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js +3 -3
- package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js.map +1 -1
- package/dist/web/bridges/mcp-app/bridge.d.ts +1 -1
- package/dist/web/bridges/mcp-app/bridge.js +9 -2
- package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
- package/dist/web/bridges/mcp-app/types.d.ts +18 -3
- package/dist/web/bridges/mcp-app/types.js.map +1 -1
- package/dist/web/bridges/mcp-app/use-mcp-app-context.d.ts +2 -2
- package/dist/web/bridges/mcp-app/use-mcp-app-context.js +1 -1
- package/dist/web/bridges/mcp-app/use-mcp-app-context.js.map +1 -1
- package/dist/web/bridges/types.d.ts +57 -4
- package/dist/web/bridges/types.js +1 -1
- package/dist/web/bridges/types.js.map +1 -1
- package/dist/web/data-llm.js +3 -2
- package/dist/web/data-llm.js.map +1 -1
- package/dist/web/data-llm.test.js +6 -0
- package/dist/web/data-llm.test.js.map +1 -1
- package/dist/web/generate-helpers.d.ts +2 -2
- package/dist/web/generate-helpers.js +2 -2
- package/dist/web/generate-helpers.js.map +1 -1
- package/dist/web/hooks/index.d.ts +5 -1
- package/dist/web/hooks/index.js +5 -1
- package/dist/web/hooks/index.js.map +1 -1
- package/dist/web/hooks/openai-extensions.test.d.ts +1 -0
- package/dist/web/hooks/openai-extensions.test.js +107 -0
- package/dist/web/hooks/openai-extensions.test.js.map +1 -0
- package/dist/web/hooks/use-call-tool.d.ts +2 -2
- package/dist/web/hooks/use-call-tool.js +15 -10
- package/dist/web/hooks/use-call-tool.js.map +1 -1
- package/dist/web/hooks/use-call-tool.test.js +13 -2
- package/dist/web/hooks/use-call-tool.test.js.map +1 -1
- package/dist/web/hooks/use-deep-link.d.ts +22 -0
- package/dist/web/hooks/use-deep-link.js +34 -0
- package/dist/web/hooks/use-deep-link.js.map +1 -0
- package/dist/web/hooks/use-file-resource.d.ts +37 -0
- package/dist/web/hooks/use-file-resource.js +98 -0
- package/dist/web/hooks/use-file-resource.js.map +1 -0
- package/dist/web/hooks/use-file-resource.test.d.ts +1 -0
- package/dist/web/hooks/use-file-resource.test.js +65 -0
- package/dist/web/hooks/use-file-resource.test.js.map +1 -0
- package/dist/web/hooks/use-host.d.ts +27 -5
- package/dist/web/hooks/use-host.js +20 -5
- package/dist/web/hooks/use-host.js.map +1 -1
- package/dist/web/hooks/use-model-context.d.ts +44 -0
- package/dist/web/hooks/use-model-context.js +63 -0
- package/dist/web/hooks/use-model-context.js.map +1 -0
- package/dist/web/hooks/use-open-file.d.ts +20 -0
- package/dist/web/hooks/use-open-file.js +25 -0
- package/dist/web/hooks/use-open-file.js.map +1 -0
- package/dist/web/hooks/use-register-view-tool.d.ts +1 -1
- package/dist/web/hooks/use-register-view-tool.js +1 -1
- package/dist/web/hooks/use-register-view-tool.js.map +1 -1
- package/dist/web/hooks/use-send-follow-up-message.d.ts +8 -2
- package/dist/web/hooks/use-send-follow-up-message.js +6 -0
- package/dist/web/hooks/use-send-follow-up-message.js.map +1 -1
- package/package.json +6 -5
package/dist/server/server.js
CHANGED
|
@@ -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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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 });
|
|
@@ -441,6 +467,9 @@ export class McpServer extends McpServerBaseOmitted {
|
|
|
441
467
|
captureToolError(extra, error);
|
|
442
468
|
throw error;
|
|
443
469
|
}
|
|
470
|
+
if (isInputRequiredResult(result)) {
|
|
471
|
+
return result;
|
|
472
|
+
}
|
|
444
473
|
warnOnLargeToolOutput(result, toolName);
|
|
445
474
|
return {
|
|
446
475
|
...result,
|
|
@@ -504,11 +533,216 @@ export class McpServer extends McpServerBaseOmitted {
|
|
|
504
533
|
cachedDiskManifest ??= JSON.parse(readFileSync(path.join(process.cwd(), "dist", "assets", ".vite", "manifest.json"), "utf-8"));
|
|
505
534
|
return cachedDiskManifest ?? {};
|
|
506
535
|
}
|
|
536
|
+
/**
|
|
537
|
+
* @experimental Register an MCP Events type that hosts can subscribe to, per
|
|
538
|
+
* the Triggers & Events working group draft. Skybridge serves `events/list`,
|
|
539
|
+
* `events/subscribe` and `events/unsubscribe`, checks auth and arguments,
|
|
540
|
+
* verifies the callback URL, then hands each subscription to `onSubscribe`.
|
|
541
|
+
* Delivering the events is up to you, for example with {@link deliverEvent}.
|
|
542
|
+
* API may change.
|
|
543
|
+
*
|
|
544
|
+
* @example
|
|
545
|
+
* ```ts
|
|
546
|
+
* server.registerEvent(
|
|
547
|
+
* {
|
|
548
|
+
* name: "comment.created",
|
|
549
|
+
* description: "Fires when someone comments on a document",
|
|
550
|
+
* inputSchema: { documentId: z.string() },
|
|
551
|
+
* payloadSchema: { documentId: z.string(), excerpt: z.string() },
|
|
552
|
+
* },
|
|
553
|
+
* {
|
|
554
|
+
* onSubscribe: ({ documentId }, { subscription }) =>
|
|
555
|
+
* webhooks.upsert({ ...subscription, filter: { documentId } }),
|
|
556
|
+
* onUnsubscribe: (_args, { subscription }) =>
|
|
557
|
+
* webhooks.remove(subscription.id),
|
|
558
|
+
* },
|
|
559
|
+
* );
|
|
560
|
+
* ```
|
|
561
|
+
*
|
|
562
|
+
* @see https://docs.skybridge.tech/api-reference/register-event
|
|
563
|
+
*/
|
|
564
|
+
registerEvent(config, hooks) {
|
|
565
|
+
if (!this.eventRegistry) {
|
|
566
|
+
this.server.registerCapabilities({ events: {} });
|
|
567
|
+
this.eventRegistry = new EventRegistry(this.server);
|
|
568
|
+
}
|
|
569
|
+
this.eventRegistry.add(config, hooks);
|
|
570
|
+
return this;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Register a ChatGPT file viewer, from the OpenAI MCP extensions: a tool
|
|
574
|
+
* with a file entrypoint for `extensions` and a view. ChatGPT desktop calls
|
|
575
|
+
* it with the file the user opened, as `{ file: { name, resourceUri } }`.
|
|
576
|
+
* Read and save the file from the view with `useFileResource`.
|
|
577
|
+
*
|
|
578
|
+
* @example
|
|
579
|
+
* ```ts
|
|
580
|
+
* server.registerFileViewer(
|
|
581
|
+
* {
|
|
582
|
+
* name: "part-viewer",
|
|
583
|
+
* title: "Part Viewer",
|
|
584
|
+
* extensions: [".stl", ".step"],
|
|
585
|
+
* view: { component: "part-viewer" },
|
|
586
|
+
* },
|
|
587
|
+
* async ({ file }) => ({ structuredContent: { name: file.name } }),
|
|
588
|
+
* );
|
|
589
|
+
* ```
|
|
590
|
+
*
|
|
591
|
+
* @see https://docs.skybridge.tech/api-reference/register-file-viewer
|
|
592
|
+
*/
|
|
593
|
+
registerFileViewer(config, cb) {
|
|
594
|
+
const { extensions, openai, ...toolConfig } = config;
|
|
595
|
+
return this.registerTool({
|
|
596
|
+
...toolConfig,
|
|
597
|
+
inputSchema: fileEntrypointInput,
|
|
598
|
+
openai: { ...openai, entrypoints: [{ type: "file", extensions }] },
|
|
599
|
+
}, cb);
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Register the composer at-mention search for ChatGPT, from the OpenAI MCP
|
|
603
|
+
* extensions. When the user types `@` and your app name, ChatGPT calls
|
|
604
|
+
* `handler` with the typed text and lists the returned resource links.
|
|
605
|
+
* Skybridge registers it as a tool hidden from the model.
|
|
606
|
+
*
|
|
607
|
+
* @example
|
|
608
|
+
* ```ts
|
|
609
|
+
* server.registerMentions({
|
|
610
|
+
* name: "search_parts",
|
|
611
|
+
* handler: async ({ query }) => ({
|
|
612
|
+
* items: findParts(query).map((part) => ({
|
|
613
|
+
* type: "resource_link",
|
|
614
|
+
* uri: `parts://${part.id}`,
|
|
615
|
+
* name: part.name,
|
|
616
|
+
* })),
|
|
617
|
+
* }),
|
|
618
|
+
* });
|
|
619
|
+
* ```
|
|
620
|
+
*
|
|
621
|
+
* @see https://docs.skybridge.tech/api-reference/register-mentions
|
|
622
|
+
*/
|
|
623
|
+
registerMentions(config) {
|
|
624
|
+
this.registerTool({
|
|
625
|
+
name: config.name,
|
|
626
|
+
description: config.description ?? "Search items to mention.",
|
|
627
|
+
annotations: { readOnlyHint: true },
|
|
628
|
+
inputSchema: { query: z.string() },
|
|
629
|
+
_meta: {
|
|
630
|
+
"openai/extensions": { "mentions/search": {} },
|
|
631
|
+
ui: { visibility: ["app"] },
|
|
632
|
+
},
|
|
633
|
+
}, async ({ query }, extra) => ({
|
|
634
|
+
content: [],
|
|
635
|
+
structuredContent: await config.handler({ query }, extra),
|
|
636
|
+
}));
|
|
637
|
+
return this;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Register app settings for ChatGPT, from the OpenAI MCP extensions.
|
|
641
|
+
* Skybridge registers a read tool and an update tool from `fields`, and
|
|
642
|
+
* advertises them in the `openai/settings` server capability. ChatGPT shows
|
|
643
|
+
* the settings page and calls `update` with the changed fields only. Store
|
|
644
|
+
* the values yourself, for example per user.
|
|
645
|
+
*
|
|
646
|
+
* @example
|
|
647
|
+
* ```ts
|
|
648
|
+
* server.registerSettings({
|
|
649
|
+
* fields: {
|
|
650
|
+
* units: { title: "Units", schema: z.enum(["mm", "in"]) },
|
|
651
|
+
* showGrid: { title: "Show grid", schema: z.boolean() },
|
|
652
|
+
* },
|
|
653
|
+
* read: (extra) => loadSettings(extra),
|
|
654
|
+
* update: (set, extra) => saveSettings(set, extra),
|
|
655
|
+
* });
|
|
656
|
+
* ```
|
|
657
|
+
*
|
|
658
|
+
* @see https://docs.skybridge.tech/api-reference/register-settings
|
|
659
|
+
*/
|
|
660
|
+
registerSettings(config) {
|
|
661
|
+
const { fields, layout, readTool = "settings.read", updateTool = "settings.update", } = config;
|
|
662
|
+
const values = z
|
|
663
|
+
.strictObject(Object.fromEntries(Object.entries(fields).map(([name, { schema, title, description }]) => [
|
|
664
|
+
name,
|
|
665
|
+
schema.meta({
|
|
666
|
+
title,
|
|
667
|
+
...(description !== undefined && { description }),
|
|
668
|
+
}),
|
|
669
|
+
])))
|
|
670
|
+
.required();
|
|
671
|
+
const { $schema: _, ...schema } = z.toJSONSchema(values);
|
|
672
|
+
for (const [name, property] of Object.entries(schema.properties ?? {})) {
|
|
673
|
+
if (fields[name]?.schema.safeParse(undefined).data !== undefined) {
|
|
674
|
+
throw new Error(`Setting "${name}" declares a default: return current values from \`read\` instead.`);
|
|
675
|
+
}
|
|
676
|
+
if (typeof property === "boolean" ||
|
|
677
|
+
!["boolean", "string", "number", "integer"].includes(String(property.type)) ||
|
|
678
|
+
(property.enum !== undefined && property.type !== "string")) {
|
|
679
|
+
throw new Error(`Setting "${name}" must be a boolean, string, string enum, number or integer.`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const placed = new Set();
|
|
683
|
+
for (const group of layout ?? []) {
|
|
684
|
+
for (const item of group.items) {
|
|
685
|
+
if (item.kind !== "property") {
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
if (!Object.hasOwn(fields, item.property) ||
|
|
689
|
+
placed.has(item.property)) {
|
|
690
|
+
throw new Error(`Settings layout names an unknown or duplicate field "${item.property}".`);
|
|
691
|
+
}
|
|
692
|
+
placed.add(item.property);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
this.registerTool({
|
|
696
|
+
name: readTool,
|
|
697
|
+
description: "Read the app settings.",
|
|
698
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
699
|
+
inputSchema: {},
|
|
700
|
+
outputSchema: {
|
|
701
|
+
schema: z.record(z.string(), z.unknown()),
|
|
702
|
+
values,
|
|
703
|
+
layout: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
704
|
+
},
|
|
705
|
+
}, async (_args, extra) => ({
|
|
706
|
+
content: [],
|
|
707
|
+
structuredContent: {
|
|
708
|
+
schema,
|
|
709
|
+
values: values.parse(await config.read(extra)),
|
|
710
|
+
layout,
|
|
711
|
+
},
|
|
712
|
+
}));
|
|
713
|
+
this.registerTool({
|
|
714
|
+
name: updateTool,
|
|
715
|
+
description: "Update the app settings.",
|
|
716
|
+
annotations: {
|
|
717
|
+
destructiveHint: false,
|
|
718
|
+
idempotentHint: true,
|
|
719
|
+
openWorldHint: false,
|
|
720
|
+
},
|
|
721
|
+
inputSchema: {
|
|
722
|
+
set: values
|
|
723
|
+
.partial()
|
|
724
|
+
.refine((set) => Object.keys(set).length > 0)
|
|
725
|
+
.meta({ minProperties: 1 }),
|
|
726
|
+
},
|
|
727
|
+
outputSchema: { values },
|
|
728
|
+
}, async ({ set }, extra) => ({
|
|
729
|
+
content: [],
|
|
730
|
+
structuredContent: {
|
|
731
|
+
values: values.parse(await config.update(set, extra)),
|
|
732
|
+
},
|
|
733
|
+
}));
|
|
734
|
+
const capability = { readTool, updateTool };
|
|
735
|
+
this.server.registerCapabilities({
|
|
736
|
+
extensions: { "openai/settings": capability },
|
|
737
|
+
experimental: { "openai/settings": capability },
|
|
738
|
+
});
|
|
739
|
+
return this;
|
|
740
|
+
}
|
|
507
741
|
registerTool(rawConfig, rawCb) {
|
|
508
742
|
const baseFn = McpServerBase.prototype.registerTool;
|
|
509
743
|
const config = rawConfig;
|
|
510
744
|
const cb = rawCb;
|
|
511
|
-
const { name, view, auth, securitySchemes: rawSecuritySchemes, _meta: userToolMeta, ...toolFields } = config;
|
|
745
|
+
const { name, view, auth, securitySchemes: rawSecuritySchemes, openai, _meta: userToolMeta, ...toolFields } = config;
|
|
512
746
|
const authNeedsProvider = auth !== undefined &&
|
|
513
747
|
(!auth.allowsAnonymous || Boolean(auth.scopes?.length));
|
|
514
748
|
if (rawSecuritySchemes === undefined &&
|
|
@@ -525,12 +759,31 @@ export class McpServer extends McpServerBaseOmitted {
|
|
|
525
759
|
// descriptor, but the SDK's `registerTool` drops unknown top-level
|
|
526
760
|
// fields, so the canonical spot isn't reachable without intercepting
|
|
527
761
|
// `tools/list`. Use the `_meta` back-compat mirror documented in the
|
|
528
|
-
//
|
|
762
|
+
// ChatGPT plugin reference until SEP-1488 lands in the spec.
|
|
529
763
|
toolMeta.securitySchemes = securitySchemes;
|
|
530
764
|
}
|
|
765
|
+
if (openai) {
|
|
766
|
+
if (!view) {
|
|
767
|
+
throw new Error(`skybridge: tool "${name}" sets \`openai\` options but has no \`view\`.`);
|
|
768
|
+
}
|
|
769
|
+
const opensWithoutInput = openai.entrypoints?.some(({ type }) => type === "global" || type === "thread");
|
|
770
|
+
if (opensWithoutInput && !acceptsEmptyInput(toolFields.inputSchema)) {
|
|
771
|
+
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.`);
|
|
772
|
+
}
|
|
773
|
+
const badExtension = openai.entrypoints
|
|
774
|
+
?.flatMap((entrypoint) => entrypoint.type === "file" ? entrypoint.extensions : [])
|
|
775
|
+
.find((extension) => !extension.startsWith("."));
|
|
776
|
+
if (badExtension !== undefined) {
|
|
777
|
+
throw new Error(`skybridge: tool "${name}" has a file entrypoint extension "${badExtension}" that doesn't start with ".".`);
|
|
778
|
+
}
|
|
779
|
+
mergeOpenAIUi(toolMeta, {
|
|
780
|
+
entrypoints: openai.entrypoints,
|
|
781
|
+
preferredModelDisplayMode: openai.preferredModelDisplayMode,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
531
784
|
if (view) {
|
|
532
785
|
this.enforceOneToolPerView(view.component, name);
|
|
533
|
-
this.registerViewResources(name, view, toolMeta);
|
|
786
|
+
this.registerViewResources(name, view, toolMeta, openai);
|
|
534
787
|
}
|
|
535
788
|
const wrappedCb = this.decorateToolHandler(cb, {
|
|
536
789
|
attachViewUUID: Boolean(view),
|