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.
- package/README.md +9 -9
- package/dist/cli/build-helpers.d.ts +1 -1
- package/dist/cli/build-helpers.js +9 -1
- package/dist/cli/build-helpers.js.map +1 -1
- package/dist/cli/build-helpers.test.js +24 -0
- package/dist/cli/build-helpers.test.js.map +1 -1
- package/dist/cli/build-steps.js +16 -5
- package/dist/cli/build-steps.js.map +1 -1
- package/dist/cli/build-steps.test.js +20 -0
- package/dist/cli/build-steps.test.js.map +1 -1
- package/dist/cli/resolve-skybridge-config.d.ts +1 -0
- package/dist/cli/resolve-skybridge-config.js +11 -1
- package/dist/cli/resolve-skybridge-config.js.map +1 -1
- 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/cli/use-typescript-check.js +4 -2
- package/dist/cli/use-typescript-check.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 +255 -10
- package/dist/server/server.js +278 -9
- package/dist/server/server.js.map +1 -1
- package/dist/server/templateHelper.d.ts +1 -0
- package/dist/server/templateHelper.js.map +1 -1
- package/dist/server/templates.generated.js +26 -12
- package/dist/server/templates.generated.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/server/view-resource-resolution.test.js +35 -0
- package/dist/server/view-resource-resolution.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/dist/web/mount-view.js +3 -5
- package/dist/web/mount-view.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 });
|
|
@@ -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
|
-
//
|
|
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),
|