authorgravatar for electricvoxel@gmail.comHwanseo Choi <electricvoxel@gmail.com> 2026-08-05 12:03:15+09:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-04 23:03:15-04:00
log3d429662c9bd7b731c059dc3a84ccdfc43bf3701
treee0604021d570cc34e6c5f643562ce07b25a9e6c0
parent7b38fb5c6b0fb46a7dadaf33e1739726be3dc104
signature Signed by PGP key B5690EEEBB952194

fix(mcp): unblock Kimi models in OpenCode with preview tools (#5128)


2 files changed, 22 insertions(+), 11 deletions(-)

apps/server/src/mcp/toolkits/preview/tools.test.ts+16
...@@ -12,6 +12,19 @@ const schemaHasDescription = (schema: unknown): boolean => {...@@ -12,6 +12,19 @@ const schemaHasDescription = (schema: unknown): boolean => {
12 .some((members) => members.some(schemaHasDescription));12 .some((members) => members.some(schemaHasDescription));
13};13};
1414
15const schemaHasMultipleAllOfDescriptions = (schema: unknown): boolean => {
16 if (!schema || typeof schema !== "object") return false;
17 const record = schema as Record<string, unknown>;
18 const allOf = Array.isArray(record.allOf) ? record.allOf : [];
19 const descriptionCount = allOf.filter(
20 (member) =>
21 member !== null &&
22 typeof member === "object" &&
23 typeof (member as Record<string, unknown>).description === "string",
24 ).length;
25 return descriptionCount > 1 || Object.values(record).some(schemaHasMultipleAllOfDescriptions);
26};
27
15it("exports provider-compatible object schemas with described parameters", () => {28it("exports provider-compatible object schemas with described parameters", () => {
16 for (const tool of Object.values(PreviewToolkit.tools)) {29 for (const tool of Object.values(PreviewToolkit.tools)) {
17 const schema = Tool.getJsonSchema(tool) as {30 const schema = Tool.getJsonSchema(tool) as {
...@@ -27,6 +40,9 @@ it("exports provider-compatible object schemas with described parameters", () =>...@@ -27,6 +40,9 @@ it("exports provider-compatible object schemas with described parameters", () =>
27 expect(schema.type, `${tool.name} must export a top-level object schema`).toBe("object");40 expect(schema.type, `${tool.name} must export a top-level object schema`).toBe("object");
28 expect(schema.anyOf, `${tool.name} must not export a root anyOf`).toBeUndefined();41 expect(schema.anyOf, `${tool.name} must not export a root anyOf`).toBeUndefined();
29 expect(schema.oneOf, `${tool.name} must not export a root oneOf`).toBeUndefined();42 expect(schema.oneOf, `${tool.name} must not export a root oneOf`).toBeUndefined();
43 if (tool.name === "preview_navigate") {
44 expect(schemaHasMultipleAllOfDescriptions(schema)).toBe(false);
45 }
30 expect(46 expect(
31 schema.properties?.tabId,47 schema.properties?.tabId,
32 `${tool.name} must allow an explicit collaborative browser tab target`,48 `${tool.name} must allow an explicit collaborative browser tab target`,
packages/contracts/src/previewAutomation.ts+6-11
...@@ -12,13 +12,10 @@ import {...@@ -12,13 +12,10 @@ import {
12import { ProviderInstanceId } from "./providerInstance.ts";12import { ProviderInstanceId } from "./providerInstance.ts";
1313
14const BoundedUrl = Schema.String.check(Schema.isTrimmed())14const BoundedUrl = Schema.String.check(Schema.isTrimmed())
15 .check(15 .check(Schema.isNonEmpty())
16 Schema.isNonEmpty({
17 description:
18 "Absolute http(s) URL or a schemeless host such as t3.chat or localhost:5173. Schemeless public hosts use https; loopback hosts use http.",
19 }),
20 )
21 .check(Schema.isMaxLength(2048));16 .check(Schema.isMaxLength(2048));
17const URL_GUIDANCE =
18 "Absolute http(s) URL or a schemeless host such as t3.chat or localhost:5173. Schemeless public hosts use https; loopback hosts use http.";
22const OptionalTimeoutMs = Schema.optional(19const OptionalTimeoutMs = Schema.optional(
23 Schema.Int.check(Schema.isGreaterThan(0))20 Schema.Int.check(Schema.isGreaterThan(0))
24 .check(Schema.isLessThanOrEqualTo(60_000))21 .check(Schema.isLessThanOrEqualTo(60_000))
...@@ -83,8 +80,7 @@ export type PreviewAutomationStatus = typeof PreviewAutomationStatus.Type;...@@ -83,8 +80,7 @@ export type PreviewAutomationStatus = typeof PreviewAutomationStatus.Type;
83export const PreviewAutomationOpenInput = Schema.Struct({80export const PreviewAutomationOpenInput = Schema.Struct({
84 ...PreviewAutomationTabTargetFields,81 ...PreviewAutomationTabTargetFields,
85 url: Schema.optional(BoundedUrl).annotate({82 url: Schema.optional(BoundedUrl).annotate({
86 description:83 description: `Optional initial page URL. ${URL_GUIDANCE} Omit to open a blank tab.`,
87 "Optional initial page URL, for example https://t3.chat or localhost:5173. Omit to open a blank tab.",
88 }),84 }),
89 open: Schema.optional(85 open: Schema.optional(
90 Schema.Boolean.annotate({86 Schema.Boolean.annotate({
...@@ -124,7 +120,7 @@ export const BrowserNavigationTarget = Schema.Union([...@@ -124,7 +120,7 @@ export const BrowserNavigationTarget = Schema.Union([
124 description: "Selects direct URL navigation.",120 description: "Selects direct URL navigation.",
125 }),121 }),
126 url: BoundedUrl.annotate({122 url: BoundedUrl.annotate({
127 description: "Direct website URL.",123 description: `Direct website URL. ${URL_GUIDANCE}`,
128 }),124 }),
129 }),125 }),
130 Schema.Struct({126 Schema.Struct({
...@@ -151,8 +147,7 @@ export type BrowserNavigationTarget = typeof BrowserNavigationTarget.Type;...@@ -151,8 +147,7 @@ export type BrowserNavigationTarget = typeof BrowserNavigationTarget.Type;
151export const PreviewAutomationNavigateInput = Schema.Struct({147export const PreviewAutomationNavigateInput = Schema.Struct({
152 ...PreviewAutomationTabTargetFields,148 ...PreviewAutomationTabTargetFields,
153 url: Schema.optional(BoundedUrl).annotate({149 url: Schema.optional(BoundedUrl).annotate({
154 description:150 description: `Website URL. ${URL_GUIDANCE} Use this for public pages and directly reachable URLs.`,
155 "Website URL, for example https://t3.chat. Use this for public pages and directly reachable URLs.",
156 }),151 }),
157 target: Schema.optional(152 target: Schema.optional(
158 BrowserNavigationTarget.annotate({153 BrowserNavigationTarget.annotate({