You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by "Shkin1 (via GitHub)" <gi...@apache.org> on 2026/09/02 08:16:52 UTC
[PR] fix: support MCP json schema tools [maka]
Shkin1 opened a new pull request, #4553:
URL: https://github.com/apache/maka/pull/4553
## Summary
Fixes #4334
Maka Desktop now accepts MCP proxy tools that expose `ai.jsonSchema()` wrappers instead of requiring every tool schema to be a Zod instance.
This change covers both parts of the failure:
- capability refresh now projects MCP tool schemas into the protocol-accepted schema vocabulary before publishing
- native tool invocation now parses arguments through the tool’s declared schema shape instead of assuming Zod only
The result is that MCP proxy tools can publish successfully and still be invoked normally at runtime.
## Verification
Ran locally:
- `npm --workspace @maka/desktop run build:test`
- `npm --workspace @maka/runtime-host run build`
- `node --test apps/desktop/dist/main/__tests__/runtime-host-native-capabilities.test.js`
- `node --test packages/runtime-host/dist/__tests__/client-capability-protocol.test.js`
## AI use
- [ ] No generative tool made a substantive contribution
- [x] Generative tooling made a substantive contribution
Tool(s) and scope:
- Codex — helped implement the fix, add regression coverage, run verification, and draft this PR description.
## Checklist
- [x] Tests cover the change and fail without it
- [x] Lint, format, typecheck and the affected suites pass locally
Does this PR entail a change in behavior?
- [x] Yes — MCP proxy tools are now accepted in the desktop capability flow and invoke successfully at runtime
- [ ] No
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix: support MCP json schema tools [maka]
Posted by "Shkin1 (via GitHub)" <gi...@apache.org>.
Shkin1 commented on code in PR #4553:
URL: https://github.com/apache/maka/pull/4553#discussion_r3913927570
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -448,29 +447,169 @@ function capabilityOffer(
}
function toolInputSchema(tool: MakaTool): Record<string, unknown> {
- const schema = toJSONSchema(requireZodSchema(tool), {
- io: "input",
- target: "draft-07",
- unrepresentable: "any",
- cycles: "ref",
- reused: "inline",
- });
- delete schema.$schema;
- if (schema.type !== "object") {
- throw new Error(
- `Desktop native capability tool schema must be an object: ${tool.name}`,
- );
+ if (tool.parameters instanceof z.ZodType) {
+ const schema = toJSONSchema(tool.parameters, {
+ io: "input",
+ target: "draft-07",
+ unrepresentable: "any",
+ cycles: "ref",
+ reused: "inline",
+ });
+ delete schema.$schema;
+ if (schema.type !== "object") {
+ throw new Error(
+ `Desktop native capability tool schema must be an object: ${tool.name}`,
+ );
+ }
+ return Object.freeze(schema);
+ }
+
+ const wrapper = tool.parameters as JsonSchemaWrapper | undefined;
+ if (wrapper && typeof wrapper.jsonSchema === "object" && wrapper.jsonSchema) {
+ const schema = wrapper.jsonSchema;
+ if (typeof schema === "object" && schema !== null && (schema as any).type === "object") {
+ return Object.freeze(cleanJsonSchemaForCapability(schema));
+ }
+ }
+
+ throw new Error(
+ `Desktop native capability tool has an unsupported schema type: ${tool.name}`,
+ );
+}
+
+const CAPABILITY_SCHEMA_KEYWORDS = new Set([
+ "$defs",
+ "$ref",
+ "additionalProperties",
+ "allOf",
+ "anyOf",
+ "const",
+ "default",
+ "definitions",
+ "description",
+ "enum",
+ "examples",
+ "exclusiveMaximum",
+ "exclusiveMinimum",
+ "format",
+ "items",
+ "maxItems",
+ "maxLength",
+ "maxProperties",
+ "maximum",
+ "minItems",
+ "minLength",
+ "minProperties",
+ "minimum",
+ "multipleOf",
+ "oneOf",
+ "pattern",
+ "patternProperties",
+ "propertyNames",
+ "properties",
+ "required",
+ "title",
+ "type",
+ "uniqueItems",
+]);
+
+function cleanJsonSchemaForCapability(schema: Record<string, unknown>): Record<string, unknown> {
+ const result: Record<string, unknown> = {};
+ for (const key of Object.keys(schema)) {
+ if (!CAPABILITY_SCHEMA_KEYWORDS.has(key)) continue;
+ result[key] = cleanSchemaKeywordValue(key, schema[key]);
+ }
+ return result;
+}
+
+function cleanSchemaKeywordValue(key: string, value: unknown): unknown {
+ if (["properties", "patternProperties", "$defs", "definitions"].includes(key)) {
+ return cleanSchemaMap(value);
}
- return Object.freeze(schema);
+ if (key === "items" || key === "additionalProperties") {
+ if (value === true || value === false) return value;
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
+ return cleanJsonSchemaForCapability(value as Record<string, unknown>);
+ }
+ }
+ return cleanSchemaValue(value);
}
-function requireZodSchema(tool: MakaTool): z.ZodType {
- if (!(tool.parameters instanceof z.ZodType)) {
- throw new Error(
- `Desktop native capability tool has an invalid schema: ${tool.name}`,
- );
+function cleanSchemaMap(value: unknown): unknown {
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return {};
+ const result: Record<string, unknown> = {};
+ for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {
+ if (nested === null || typeof nested !== "object" || Array.isArray(nested)) {
+ result[key] = nested;
+ continue;
+ }
+ result[key] = cleanJsonSchemaForCapability(nested as Record<string, unknown>);
+ }
+ return result;
+}
+
+function cleanSchemaValue(value: unknown): unknown {
Review Comment:
Thanks, that makes sense. I’ll align the PR with this direction and consolidate the schema handling layer.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix: support MCP json schema tools [maka]
Posted by "liugddx (via GitHub)" <gi...@apache.org>.
liugddx commented on code in PR #4553:
URL: https://github.com/apache/maka/pull/4553#discussion_r3912640356
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -337,8 +337,7 @@ async function invokeNativeTool(
}
const signal = AbortSignal.any([options.signal, invocation.signal]);
signal.throwIfAborted();
- const parameters = requireZodSchema(binding.tool);
- const args = await parameters.parseAsync(frame.arguments);
+ const args = await parseNativeToolArguments(binding.tool.parameters, frame.arguments);
Review Comment:
This is the standing invariant that changes: on `main` this was `requireZodSchema(...).parseAsync(...)`, which validated every tool's arguments at the trust boundary. For an `ai.jsonSchema()` wrapper with no `validate` option (the MCP proxy case), `parseNativeToolArguments` matches none of its branches and returns `args` untouched — so arguments reach `binding.tool.impl` unvalidated. Either compile the JSON Schema and validate (e.g. via `safeValidateTypes` from `@ai-sdk/provider-utils`, already a dependency), or make the delegation-to-MCP-server intent explicit in code + test.
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -448,29 +447,169 @@ function capabilityOffer(
}
function toolInputSchema(tool: MakaTool): Record<string, unknown> {
- const schema = toJSONSchema(requireZodSchema(tool), {
- io: "input",
- target: "draft-07",
- unrepresentable: "any",
- cycles: "ref",
- reused: "inline",
- });
- delete schema.$schema;
- if (schema.type !== "object") {
- throw new Error(
- `Desktop native capability tool schema must be an object: ${tool.name}`,
- );
+ if (tool.parameters instanceof z.ZodType) {
+ const schema = toJSONSchema(tool.parameters, {
+ io: "input",
+ target: "draft-07",
+ unrepresentable: "any",
+ cycles: "ref",
+ reused: "inline",
+ });
+ delete schema.$schema;
+ if (schema.type !== "object") {
+ throw new Error(
+ `Desktop native capability tool schema must be an object: ${tool.name}`,
+ );
+ }
+ return Object.freeze(schema);
+ }
+
+ const wrapper = tool.parameters as JsonSchemaWrapper | undefined;
+ if (wrapper && typeof wrapper.jsonSchema === "object" && wrapper.jsonSchema) {
+ const schema = wrapper.jsonSchema;
+ if (typeof schema === "object" && schema !== null && (schema as any).type === "object") {
+ return Object.freeze(cleanJsonSchemaForCapability(schema));
+ }
+ }
+
+ throw new Error(
+ `Desktop native capability tool has an unsupported schema type: ${tool.name}`,
+ );
+}
+
+const CAPABILITY_SCHEMA_KEYWORDS = new Set([
+ "$defs",
+ "$ref",
+ "additionalProperties",
+ "allOf",
+ "anyOf",
+ "const",
+ "default",
+ "definitions",
+ "description",
+ "enum",
+ "examples",
+ "exclusiveMaximum",
+ "exclusiveMinimum",
+ "format",
+ "items",
+ "maxItems",
+ "maxLength",
+ "maxProperties",
+ "maximum",
+ "minItems",
+ "minLength",
+ "minProperties",
+ "minimum",
+ "multipleOf",
+ "oneOf",
+ "pattern",
+ "patternProperties",
+ "propertyNames",
+ "properties",
+ "required",
+ "title",
+ "type",
+ "uniqueItems",
+]);
+
+function cleanJsonSchemaForCapability(schema: Record<string, unknown>): Record<string, unknown> {
+ const result: Record<string, unknown> = {};
+ for (const key of Object.keys(schema)) {
+ if (!CAPABILITY_SCHEMA_KEYWORDS.has(key)) continue;
+ result[key] = cleanSchemaKeywordValue(key, schema[key]);
+ }
+ return result;
+}
+
+function cleanSchemaKeywordValue(key: string, value: unknown): unknown {
+ if (["properties", "patternProperties", "$defs", "definitions"].includes(key)) {
+ return cleanSchemaMap(value);
}
- return Object.freeze(schema);
+ if (key === "items" || key === "additionalProperties") {
+ if (value === true || value === false) return value;
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
+ return cleanJsonSchemaForCapability(value as Record<string, unknown>);
+ }
+ }
+ return cleanSchemaValue(value);
}
-function requireZodSchema(tool: MakaTool): z.ZodType {
- if (!(tool.parameters instanceof z.ZodType)) {
- throw new Error(
- `Desktop native capability tool has an invalid schema: ${tool.name}`,
- );
+function cleanSchemaMap(value: unknown): unknown {
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return {};
+ const result: Record<string, unknown> = {};
+ for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {
+ if (nested === null || typeof nested !== "object" || Array.isArray(nested)) {
+ result[key] = nested;
+ continue;
+ }
+ result[key] = cleanJsonSchemaForCapability(nested as Record<string, unknown>);
+ }
+ return result;
+}
+
+function cleanSchemaValue(value: unknown): unknown {
Review Comment:
`cleanSchemaValue` treats non-schema JSON values as schemas. `cleanSchemaKeywordValue` routes `const`/`default`/`enum` members/`examples` here, and for any object this calls `cleanJsonSchemaForCapability`, which prunes every key not in the keyword allowlist. So `default: { retries: 3, verbose: true }` publishes as `default: {}`, and `enum: [{status:'a'}]` as `[{}]` — silently, since the protocol validator doesn't inspect those contents. These four keywords carry arbitrary JSON and should be deep-cloned through unchanged, not key-pruned.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix: support MCP json schema tools [maka]
Posted by "Astro-Han (via GitHub)" <gi...@apache.org>.
Astro-Han commented on code in PR #4553:
URL: https://github.com/apache/maka/pull/4553#discussion_r3927054156
##########
packages/runtime-host/src/protocol/index.ts:
##########
@@ -100,7 +100,11 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1 as const;
export const RUNTIME_HOST_PROTOCOL_VERSION = 0 as const;
// Increment when the same protocol version no longer guarantees safe Client-Host
// interoperability. Mismatches are rejected before domain commands are admitted.
-export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 105 as const;
+export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 106 as const;
Review Comment:
P1: this must be rebased before it can merge. Current `main` (`ab7b7392`) already declares epoch 109, this head declares 106, and `git merge-tree` reports this file as the only content conflict. Resolving it by keeping 109 still fails `scripts/protocol-epoch-check.mjs`, because `client-capability.ts` changes without a compatible-change declaration; keeping 106 moves the epoch backward. Rebase, preserve main's 106-109 comment history, and allocate 110 for this change.
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -419,82 +419,260 @@ function abortInvocations(
return settling;
}
-function capabilityOffer(
- group: DesktopCapabilityGroup,
+function buildPublishedCapabilities(
+ groups: readonly DesktopCapabilityGroup[],
hostPathAccess: ClientCapabilityHostPathAccess,
-): ClientCapabilityOffer {
- return Object.freeze({
- offerId: group.offerId,
- version: CAPABILITY_VERSION,
- affinity: "session",
- hostPathAccess,
- label: group.label,
- description: group.description,
- tools: Object.freeze(
- group.tools.map((tool) =>
+): {
+ readonly offers: readonly ClientCapabilityOffer[];
+ readonly bindings: Map<string, NativeToolBinding>;
+} {
+ const offers: ClientCapabilityOffer[] = [];
+ const bindings = new Map<string, NativeToolBinding>();
+ for (const group of groups) {
+ const tools: ClientCapabilityToolDescriptor[] = [];
+ for (const tool of group.tools) {
+ const key = bindingKey({
+ offerId: group.offerId,
+ serverId: group.offerId,
+ toolName: tool.name,
+ });
+ if (bindings.has(key)) {
+ throw new Error(
+ `Duplicate Desktop native capability tool: ${group.offerId}/${tool.name}`,
+ );
+ }
+ let inputSchema: Record<string, unknown>;
+ try {
+ inputSchema = toolInputSchema(tool);
+ validateToolInputSchema(inputSchema);
+ } catch (error) {
+ // One malformed MCP descriptor must not take down the whole offer set:
+ // skip and name the offending tool so Browser, Computer Use, settings,
+ // and other MCP tools keep publishing.
+ console.warn(
+ `Skipping Desktop native capability tool ${group.offerId}/${tool.name}: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ continue;
+ }
+ bindings.set(key, { tool });
+ tools.push(
Object.freeze({
serverId: group.offerId,
name: tool.name,
description: tool.description,
- inputSchema: toolInputSchema(tool),
+ inputSchema,
...(tool.activityKind ? { activityKind: tool.activityKind } : {}),
...(tool.displayName
? { annotations: Object.freeze({ title: tool.displayName }) }
: {}),
}),
- ),
- ),
- });
+ );
+ }
+ if (tools.length === 0) continue;
+ offers.push(
+ Object.freeze({
+ offerId: group.offerId,
+ version: CAPABILITY_VERSION,
+ affinity: "session",
+ hostPathAccess,
+ label: group.label,
+ description: group.description,
+ tools: Object.freeze(tools),
+ }),
+ );
+ }
+ return { offers: Object.freeze(offers), bindings };
}
function toolInputSchema(tool: MakaTool): Record<string, unknown> {
- const schema = toJSONSchema(requireZodSchema(tool), {
- io: "input",
- target: "draft-07",
- unrepresentable: "any",
- cycles: "ref",
- reused: "inline",
- });
- delete schema.$schema;
- if (schema.type !== "object") {
+ if (tool.parameters instanceof z.ZodType) {
+ const schema = toJSONSchema(tool.parameters, {
+ io: "input",
+ target: "draft-07",
+ unrepresentable: "any",
+ cycles: "ref",
+ reused: "inline",
+ });
+ delete schema.$schema;
+ if (schema.type !== "object") {
+ throw new Error(
+ `Desktop native capability tool schema must be an object: ${tool.name}`,
+ );
+ }
+ return Object.freeze(schema);
+ }
+
+ const wrapper = tool.parameters as JsonSchemaWrapper | undefined;
+ if (wrapper && typeof wrapper.jsonSchema === "object" && wrapper.jsonSchema) {
+ const schema = wrapper.jsonSchema;
+ if (typeof schema === "object" && schema !== null) {
+ return Object.freeze(
+ projectToolInputSchema(schema as Record<string, unknown>),
+ );
+ }
+ }
+
+ throw new Error(
+ `Desktop native capability tool has an unsupported schema type: ${tool.name}`,
+ );
+}
+
+interface JsonSchemaWrapper {
+ readonly jsonSchema?: Record<string, unknown>;
+}
+
+// -- JSON Schema argument validation -------------------------------------------
+//
+// Validation runs against the projected (advertised) schema, not the raw
+// MCP schema, so any constraint expressed via non-whitelisted keywords
+// (if/then/else, contains, dependentRequired, …) is dropped before Ajv
+// sees it. The downstream MCP server re-validates against the full schema.
+//
+// Regex constraints (pattern / patternProperties) are also omitted from the
+// local validator: the schema comes from an untrusted MCP server, and Ajv
+// executes those expressions synchronously on the call path, so a
+// pathological expression could block the Electron main process (ReDoS).
+// The MCP endpoint enforces them against the full schema.
+
+const jsonSchemaValidatorOptions = {
+ allErrors: true,
+ strict: false,
+ validateFormats: false,
+} as const;
+const schemaValidator = new Ajv2020(jsonSchemaValidatorOptions);
+const compiledSchemas = new WeakMap<object, ValidateFunction>();
+
+function adaptSchemaForLocalValidation(value: unknown): unknown {
+ if (value === null || typeof value !== 'object') return value;
+ if (Array.isArray(value)) return value.map((entry) => adaptSchemaForLocalValidation(entry));
+ const schema = value as Record<string, unknown>;
+ const result: Record<string, unknown> = {};
+ for (const [key, val] of Object.entries(schema)) {
+ if (key === 'pattern' || key === 'patternProperties') continue;
Review Comment:
P2: dropping `patternProperties` while `additionalProperties` is passed through unchanged at line 586 makes the local validator stricter than the schema you advertise. For `{type:'object', patternProperties:{'^x-':{type:'string'}}, additionalProperties:false}` the published schema accepts `{"x-foo":"bar"}`, but local Ajv sees an unmatched property and the call throws `Invalid arguments` before ever reaching the MCP server, so the tool registers and is never callable. `patternProperties` is the keyword this PR adds, so this pairing is the expected case, not an exotic one. liugddx raised this on this exact head (comment 3924949223). If the local validator goes away with the P1 fix this resolves itself; if it stays, relax `additionalProperties` to `true` whenever `patternProperties` is dropped.
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -419,82 +419,260 @@ function abortInvocations(
return settling;
}
-function capabilityOffer(
- group: DesktopCapabilityGroup,
+function buildPublishedCapabilities(
+ groups: readonly DesktopCapabilityGroup[],
hostPathAccess: ClientCapabilityHostPathAccess,
-): ClientCapabilityOffer {
- return Object.freeze({
- offerId: group.offerId,
- version: CAPABILITY_VERSION,
- affinity: "session",
- hostPathAccess,
- label: group.label,
- description: group.description,
- tools: Object.freeze(
- group.tools.map((tool) =>
+): {
+ readonly offers: readonly ClientCapabilityOffer[];
+ readonly bindings: Map<string, NativeToolBinding>;
+} {
+ const offers: ClientCapabilityOffer[] = [];
+ const bindings = new Map<string, NativeToolBinding>();
+ for (const group of groups) {
+ const tools: ClientCapabilityToolDescriptor[] = [];
+ for (const tool of group.tools) {
+ const key = bindingKey({
+ offerId: group.offerId,
+ serverId: group.offerId,
+ toolName: tool.name,
+ });
+ if (bindings.has(key)) {
+ throw new Error(
+ `Duplicate Desktop native capability tool: ${group.offerId}/${tool.name}`,
+ );
+ }
+ let inputSchema: Record<string, unknown>;
+ try {
+ inputSchema = toolInputSchema(tool);
+ validateToolInputSchema(inputSchema);
+ } catch (error) {
+ // One malformed MCP descriptor must not take down the whole offer set:
+ // skip and name the offending tool so Browser, Computer Use, settings,
+ // and other MCP tools keep publishing.
+ console.warn(
+ `Skipping Desktop native capability tool ${group.offerId}/${tool.name}: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ continue;
+ }
+ bindings.set(key, { tool });
+ tools.push(
Object.freeze({
serverId: group.offerId,
name: tool.name,
description: tool.description,
- inputSchema: toolInputSchema(tool),
+ inputSchema,
...(tool.activityKind ? { activityKind: tool.activityKind } : {}),
...(tool.displayName
? { annotations: Object.freeze({ title: tool.displayName }) }
: {}),
}),
- ),
- ),
- });
+ );
+ }
+ if (tools.length === 0) continue;
+ offers.push(
+ Object.freeze({
+ offerId: group.offerId,
+ version: CAPABILITY_VERSION,
+ affinity: "session",
+ hostPathAccess,
+ label: group.label,
+ description: group.description,
+ tools: Object.freeze(tools),
+ }),
+ );
+ }
+ return { offers: Object.freeze(offers), bindings };
}
function toolInputSchema(tool: MakaTool): Record<string, unknown> {
- const schema = toJSONSchema(requireZodSchema(tool), {
- io: "input",
- target: "draft-07",
- unrepresentable: "any",
- cycles: "ref",
- reused: "inline",
- });
- delete schema.$schema;
- if (schema.type !== "object") {
+ if (tool.parameters instanceof z.ZodType) {
+ const schema = toJSONSchema(tool.parameters, {
+ io: "input",
+ target: "draft-07",
+ unrepresentable: "any",
+ cycles: "ref",
+ reused: "inline",
+ });
+ delete schema.$schema;
+ if (schema.type !== "object") {
+ throw new Error(
+ `Desktop native capability tool schema must be an object: ${tool.name}`,
+ );
+ }
+ return Object.freeze(schema);
+ }
+
+ const wrapper = tool.parameters as JsonSchemaWrapper | undefined;
+ if (wrapper && typeof wrapper.jsonSchema === "object" && wrapper.jsonSchema) {
+ const schema = wrapper.jsonSchema;
+ if (typeof schema === "object" && schema !== null) {
+ return Object.freeze(
+ projectToolInputSchema(schema as Record<string, unknown>),
+ );
+ }
+ }
+
+ throw new Error(
+ `Desktop native capability tool has an unsupported schema type: ${tool.name}`,
+ );
+}
+
+interface JsonSchemaWrapper {
+ readonly jsonSchema?: Record<string, unknown>;
+}
+
+// -- JSON Schema argument validation -------------------------------------------
+//
+// Validation runs against the projected (advertised) schema, not the raw
+// MCP schema, so any constraint expressed via non-whitelisted keywords
+// (if/then/else, contains, dependentRequired, …) is dropped before Ajv
+// sees it. The downstream MCP server re-validates against the full schema.
+//
+// Regex constraints (pattern / patternProperties) are also omitted from the
+// local validator: the schema comes from an untrusted MCP server, and Ajv
+// executes those expressions synchronously on the call path, so a
+// pathological expression could block the Electron main process (ReDoS).
+// The MCP endpoint enforces them against the full schema.
+
+const jsonSchemaValidatorOptions = {
+ allErrors: true,
+ strict: false,
+ validateFormats: false,
+} as const;
+const schemaValidator = new Ajv2020(jsonSchemaValidatorOptions);
Review Comment:
P1: this is a second copy of an existing owner. `packages/runtime/src/ai-sdk-backend.ts:576-660` already compiles `parameters.jsonSchema` with Ajv, dispatches on `$schema` across draft-07 / 2019-09 / 2020-12, caches in a `WeakMap`, and has a `schemaErrorSummary` of the same name; this block reproduces all of it minus the dialect dispatch, which is exactly why `adaptSchemaForLocalValidation` below has to hand-translate draft-07 tuples and re-encode the keyword-to-shape table a third time. Smallest complete fix is upstream: `packages/runtime/src/mcp-tools.ts:117` is the one place an MCP `inputSchema` becomes tool parameters, so build it as `jsonSchema(descriptor.inputSchema, { validate })`. That makes the existing `validate` branch in `validateDeclaredToolArgs` (`packages/runtime/src/tool-runtime.ts:3021-3066`) fire, covers CLI MCP tools too, and reduces this file to calling `wrapper.validate`, deleting lines 524-618 and the new `ajv` dependency. If validation has to stay in the Ele
ctron main process, reuse the dispatch and cache from `ai-sdk-backend.ts` rather than rewriting them, and record the reason here.
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -419,82 +419,260 @@ function abortInvocations(
return settling;
}
-function capabilityOffer(
- group: DesktopCapabilityGroup,
+function buildPublishedCapabilities(
+ groups: readonly DesktopCapabilityGroup[],
hostPathAccess: ClientCapabilityHostPathAccess,
-): ClientCapabilityOffer {
- return Object.freeze({
- offerId: group.offerId,
- version: CAPABILITY_VERSION,
- affinity: "session",
- hostPathAccess,
- label: group.label,
- description: group.description,
- tools: Object.freeze(
- group.tools.map((tool) =>
+): {
+ readonly offers: readonly ClientCapabilityOffer[];
+ readonly bindings: Map<string, NativeToolBinding>;
+} {
+ const offers: ClientCapabilityOffer[] = [];
+ const bindings = new Map<string, NativeToolBinding>();
+ for (const group of groups) {
+ const tools: ClientCapabilityToolDescriptor[] = [];
+ for (const tool of group.tools) {
+ const key = bindingKey({
+ offerId: group.offerId,
+ serverId: group.offerId,
+ toolName: tool.name,
+ });
+ if (bindings.has(key)) {
+ throw new Error(
+ `Duplicate Desktop native capability tool: ${group.offerId}/${tool.name}`,
+ );
+ }
+ let inputSchema: Record<string, unknown>;
+ try {
+ inputSchema = toolInputSchema(tool);
+ validateToolInputSchema(inputSchema);
+ } catch (error) {
+ // One malformed MCP descriptor must not take down the whole offer set:
+ // skip and name the offending tool so Browser, Computer Use, settings,
+ // and other MCP tools keep publishing.
+ console.warn(
Review Comment:
P2: per-tool isolation is right for untrusted MCP descriptors, but this catch covers every group, including Browser, Computer Use, settings, and Rive. A first-party tool with a bad schema now disappears behind a `console.warn` nobody sees instead of failing at startup, which is the invariant `runtime-host-desktop-candidate.test.ts:547-570` used to hold. On the MCP side the same silence is user-visible as a tool that simply is not there: an MCP schema omitting the top-level `type` is rejected by `projectToolInputSchema` (`client-capability.ts:775-780`) and dropped with no signal. Suggest scoping the try/catch to the MCP group, keeping first-party groups fatal, surfacing skips through a real Desktop diagnostic path, and defaulting a missing MCP root `type` to `"object"` before projecting.
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -419,82 +419,260 @@ function abortInvocations(
return settling;
}
-function capabilityOffer(
- group: DesktopCapabilityGroup,
+function buildPublishedCapabilities(
+ groups: readonly DesktopCapabilityGroup[],
hostPathAccess: ClientCapabilityHostPathAccess,
-): ClientCapabilityOffer {
- return Object.freeze({
- offerId: group.offerId,
- version: CAPABILITY_VERSION,
- affinity: "session",
- hostPathAccess,
- label: group.label,
- description: group.description,
- tools: Object.freeze(
- group.tools.map((tool) =>
+): {
+ readonly offers: readonly ClientCapabilityOffer[];
+ readonly bindings: Map<string, NativeToolBinding>;
+} {
+ const offers: ClientCapabilityOffer[] = [];
+ const bindings = new Map<string, NativeToolBinding>();
+ for (const group of groups) {
+ const tools: ClientCapabilityToolDescriptor[] = [];
+ for (const tool of group.tools) {
+ const key = bindingKey({
+ offerId: group.offerId,
+ serverId: group.offerId,
+ toolName: tool.name,
+ });
+ if (bindings.has(key)) {
+ throw new Error(
+ `Duplicate Desktop native capability tool: ${group.offerId}/${tool.name}`,
+ );
+ }
+ let inputSchema: Record<string, unknown>;
+ try {
+ inputSchema = toolInputSchema(tool);
+ validateToolInputSchema(inputSchema);
+ } catch (error) {
+ // One malformed MCP descriptor must not take down the whole offer set:
+ // skip and name the offending tool so Browser, Computer Use, settings,
+ // and other MCP tools keep publishing.
+ console.warn(
+ `Skipping Desktop native capability tool ${group.offerId}/${tool.name}: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ continue;
+ }
+ bindings.set(key, { tool });
+ tools.push(
Object.freeze({
serverId: group.offerId,
name: tool.name,
description: tool.description,
- inputSchema: toolInputSchema(tool),
+ inputSchema,
...(tool.activityKind ? { activityKind: tool.activityKind } : {}),
...(tool.displayName
? { annotations: Object.freeze({ title: tool.displayName }) }
: {}),
}),
- ),
- ),
- });
+ );
+ }
+ if (tools.length === 0) continue;
+ offers.push(
+ Object.freeze({
+ offerId: group.offerId,
+ version: CAPABILITY_VERSION,
+ affinity: "session",
+ hostPathAccess,
+ label: group.label,
+ description: group.description,
+ tools: Object.freeze(tools),
+ }),
+ );
+ }
+ return { offers: Object.freeze(offers), bindings };
}
function toolInputSchema(tool: MakaTool): Record<string, unknown> {
- const schema = toJSONSchema(requireZodSchema(tool), {
- io: "input",
- target: "draft-07",
- unrepresentable: "any",
- cycles: "ref",
- reused: "inline",
- });
- delete schema.$schema;
- if (schema.type !== "object") {
+ if (tool.parameters instanceof z.ZodType) {
+ const schema = toJSONSchema(tool.parameters, {
+ io: "input",
+ target: "draft-07",
+ unrepresentable: "any",
+ cycles: "ref",
+ reused: "inline",
+ });
+ delete schema.$schema;
+ if (schema.type !== "object") {
+ throw new Error(
+ `Desktop native capability tool schema must be an object: ${tool.name}`,
+ );
+ }
+ return Object.freeze(schema);
+ }
+
+ const wrapper = tool.parameters as JsonSchemaWrapper | undefined;
+ if (wrapper && typeof wrapper.jsonSchema === "object" && wrapper.jsonSchema) {
+ const schema = wrapper.jsonSchema;
+ if (typeof schema === "object" && schema !== null) {
+ return Object.freeze(
+ projectToolInputSchema(schema as Record<string, unknown>),
+ );
+ }
+ }
+
+ throw new Error(
+ `Desktop native capability tool has an unsupported schema type: ${tool.name}`,
+ );
+}
+
+interface JsonSchemaWrapper {
+ readonly jsonSchema?: Record<string, unknown>;
+}
+
+// -- JSON Schema argument validation -------------------------------------------
+//
+// Validation runs against the projected (advertised) schema, not the raw
+// MCP schema, so any constraint expressed via non-whitelisted keywords
+// (if/then/else, contains, dependentRequired, …) is dropped before Ajv
+// sees it. The downstream MCP server re-validates against the full schema.
+//
+// Regex constraints (pattern / patternProperties) are also omitted from the
+// local validator: the schema comes from an untrusted MCP server, and Ajv
+// executes those expressions synchronously on the call path, so a
+// pathological expression could block the Electron main process (ReDoS).
+// The MCP endpoint enforces them against the full schema.
+
+const jsonSchemaValidatorOptions = {
+ allErrors: true,
+ strict: false,
+ validateFormats: false,
+} as const;
+const schemaValidator = new Ajv2020(jsonSchemaValidatorOptions);
+const compiledSchemas = new WeakMap<object, ValidateFunction>();
+
+function adaptSchemaForLocalValidation(value: unknown): unknown {
+ if (value === null || typeof value !== 'object') return value;
+ if (Array.isArray(value)) return value.map((entry) => adaptSchemaForLocalValidation(entry));
+ const schema = value as Record<string, unknown>;
+ const result: Record<string, unknown> = {};
+ for (const [key, val] of Object.entries(schema)) {
+ if (key === 'pattern' || key === 'patternProperties') continue;
+ if (key === 'items' && Array.isArray(val)) {
+ // draft-07 tuple `items` does not compile under Ajv 2020-12 (which
+ // requires a single schema or `prefixItems`), so translate it before
+ // compiling. The advertised schema keeps the original tuple shape.
+ if ((val as unknown[]).length === 0) continue;
+ result.prefixItems = (val as unknown[]).map((entry) =>
+ adaptSchemaForLocalValidation(entry),
+ );
+ result.items = false;
Review Comment:
P3: a draft-07 tuple `items: [A, B]` with no `additionalItems` permits extra trailing items, but `items: false` forbids them, so the advertised schema accepts `[1,2,3]` and the local validator rejects it. Same direction as the `patternProperties` issue above: local enforcement exceeding what was published. Omit `items` (or set it to `true`). Also raised on this head as comment 3924949230.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix: support MCP json schema tools [maka]
Posted by "liugddx (via GitHub)" <gi...@apache.org>.
liugddx commented on code in PR #4553:
URL: https://github.com/apache/maka/pull/4553#discussion_r3924949230
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -419,82 +419,260 @@ function abortInvocations(
return settling;
}
-function capabilityOffer(
- group: DesktopCapabilityGroup,
+function buildPublishedCapabilities(
+ groups: readonly DesktopCapabilityGroup[],
hostPathAccess: ClientCapabilityHostPathAccess,
-): ClientCapabilityOffer {
- return Object.freeze({
- offerId: group.offerId,
- version: CAPABILITY_VERSION,
- affinity: "session",
- hostPathAccess,
- label: group.label,
- description: group.description,
- tools: Object.freeze(
- group.tools.map((tool) =>
+): {
+ readonly offers: readonly ClientCapabilityOffer[];
+ readonly bindings: Map<string, NativeToolBinding>;
+} {
+ const offers: ClientCapabilityOffer[] = [];
+ const bindings = new Map<string, NativeToolBinding>();
+ for (const group of groups) {
+ const tools: ClientCapabilityToolDescriptor[] = [];
+ for (const tool of group.tools) {
+ const key = bindingKey({
+ offerId: group.offerId,
+ serverId: group.offerId,
+ toolName: tool.name,
+ });
+ if (bindings.has(key)) {
+ throw new Error(
+ `Duplicate Desktop native capability tool: ${group.offerId}/${tool.name}`,
+ );
+ }
+ let inputSchema: Record<string, unknown>;
+ try {
+ inputSchema = toolInputSchema(tool);
+ validateToolInputSchema(inputSchema);
+ } catch (error) {
+ // One malformed MCP descriptor must not take down the whole offer set:
+ // skip and name the offending tool so Browser, Computer Use, settings,
+ // and other MCP tools keep publishing.
+ console.warn(
+ `Skipping Desktop native capability tool ${group.offerId}/${tool.name}: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ continue;
+ }
+ bindings.set(key, { tool });
+ tools.push(
Object.freeze({
serverId: group.offerId,
name: tool.name,
description: tool.description,
- inputSchema: toolInputSchema(tool),
+ inputSchema,
...(tool.activityKind ? { activityKind: tool.activityKind } : {}),
...(tool.displayName
? { annotations: Object.freeze({ title: tool.displayName }) }
: {}),
}),
- ),
- ),
- });
+ );
+ }
+ if (tools.length === 0) continue;
+ offers.push(
+ Object.freeze({
+ offerId: group.offerId,
+ version: CAPABILITY_VERSION,
+ affinity: "session",
+ hostPathAccess,
+ label: group.label,
+ description: group.description,
+ tools: Object.freeze(tools),
+ }),
+ );
+ }
+ return { offers: Object.freeze(offers), bindings };
}
function toolInputSchema(tool: MakaTool): Record<string, unknown> {
- const schema = toJSONSchema(requireZodSchema(tool), {
- io: "input",
- target: "draft-07",
- unrepresentable: "any",
- cycles: "ref",
- reused: "inline",
- });
- delete schema.$schema;
- if (schema.type !== "object") {
+ if (tool.parameters instanceof z.ZodType) {
+ const schema = toJSONSchema(tool.parameters, {
+ io: "input",
+ target: "draft-07",
+ unrepresentable: "any",
+ cycles: "ref",
+ reused: "inline",
+ });
+ delete schema.$schema;
+ if (schema.type !== "object") {
+ throw new Error(
+ `Desktop native capability tool schema must be an object: ${tool.name}`,
+ );
+ }
+ return Object.freeze(schema);
+ }
+
+ const wrapper = tool.parameters as JsonSchemaWrapper | undefined;
+ if (wrapper && typeof wrapper.jsonSchema === "object" && wrapper.jsonSchema) {
+ const schema = wrapper.jsonSchema;
+ if (typeof schema === "object" && schema !== null) {
+ return Object.freeze(
+ projectToolInputSchema(schema as Record<string, unknown>),
+ );
+ }
+ }
+
+ throw new Error(
+ `Desktop native capability tool has an unsupported schema type: ${tool.name}`,
+ );
+}
+
+interface JsonSchemaWrapper {
+ readonly jsonSchema?: Record<string, unknown>;
+}
+
+// -- JSON Schema argument validation -------------------------------------------
+//
+// Validation runs against the projected (advertised) schema, not the raw
+// MCP schema, so any constraint expressed via non-whitelisted keywords
+// (if/then/else, contains, dependentRequired, …) is dropped before Ajv
+// sees it. The downstream MCP server re-validates against the full schema.
+//
+// Regex constraints (pattern / patternProperties) are also omitted from the
+// local validator: the schema comes from an untrusted MCP server, and Ajv
+// executes those expressions synchronously on the call path, so a
+// pathological expression could block the Electron main process (ReDoS).
+// The MCP endpoint enforces them against the full schema.
+
+const jsonSchemaValidatorOptions = {
+ allErrors: true,
+ strict: false,
+ validateFormats: false,
+} as const;
+const schemaValidator = new Ajv2020(jsonSchemaValidatorOptions);
+const compiledSchemas = new WeakMap<object, ValidateFunction>();
+
+function adaptSchemaForLocalValidation(value: unknown): unknown {
+ if (value === null || typeof value !== 'object') return value;
+ if (Array.isArray(value)) return value.map((entry) => adaptSchemaForLocalValidation(entry));
+ const schema = value as Record<string, unknown>;
+ const result: Record<string, unknown> = {};
+ for (const [key, val] of Object.entries(schema)) {
+ if (key === 'pattern' || key === 'patternProperties') continue;
+ if (key === 'items' && Array.isArray(val)) {
+ // draft-07 tuple `items` does not compile under Ajv 2020-12 (which
+ // requires a single schema or `prefixItems`), so translate it before
+ // compiling. The advertised schema keeps the original tuple shape.
+ if ((val as unknown[]).length === 0) continue;
+ result.prefixItems = (val as unknown[]).map((entry) =>
+ adaptSchemaForLocalValidation(entry),
+ );
+ result.items = false;
Review Comment:
**P3 (blocking).** draft-07 tuple `items:[…]` with no `additionalItems` permits extra trailing items; `items:false` forbids them. For `items:[{integer},{integer}]` the advertised schema accepts `[1,2,3]` but the local validator rejects it. Use `items: true` (or omit `items`) so the local check matches what you advertise.
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -419,82 +419,260 @@ function abortInvocations(
return settling;
}
-function capabilityOffer(
- group: DesktopCapabilityGroup,
+function buildPublishedCapabilities(
+ groups: readonly DesktopCapabilityGroup[],
hostPathAccess: ClientCapabilityHostPathAccess,
-): ClientCapabilityOffer {
- return Object.freeze({
- offerId: group.offerId,
- version: CAPABILITY_VERSION,
- affinity: "session",
- hostPathAccess,
- label: group.label,
- description: group.description,
- tools: Object.freeze(
- group.tools.map((tool) =>
+): {
+ readonly offers: readonly ClientCapabilityOffer[];
+ readonly bindings: Map<string, NativeToolBinding>;
+} {
+ const offers: ClientCapabilityOffer[] = [];
+ const bindings = new Map<string, NativeToolBinding>();
+ for (const group of groups) {
+ const tools: ClientCapabilityToolDescriptor[] = [];
+ for (const tool of group.tools) {
+ const key = bindingKey({
+ offerId: group.offerId,
+ serverId: group.offerId,
+ toolName: tool.name,
+ });
+ if (bindings.has(key)) {
+ throw new Error(
+ `Duplicate Desktop native capability tool: ${group.offerId}/${tool.name}`,
+ );
+ }
+ let inputSchema: Record<string, unknown>;
+ try {
+ inputSchema = toolInputSchema(tool);
+ validateToolInputSchema(inputSchema);
+ } catch (error) {
+ // One malformed MCP descriptor must not take down the whole offer set:
+ // skip and name the offending tool so Browser, Computer Use, settings,
+ // and other MCP tools keep publishing.
+ console.warn(
+ `Skipping Desktop native capability tool ${group.offerId}/${tool.name}: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ continue;
+ }
+ bindings.set(key, { tool });
+ tools.push(
Object.freeze({
serverId: group.offerId,
name: tool.name,
description: tool.description,
- inputSchema: toolInputSchema(tool),
+ inputSchema,
...(tool.activityKind ? { activityKind: tool.activityKind } : {}),
...(tool.displayName
? { annotations: Object.freeze({ title: tool.displayName }) }
: {}),
}),
- ),
- ),
- });
+ );
+ }
+ if (tools.length === 0) continue;
+ offers.push(
+ Object.freeze({
+ offerId: group.offerId,
+ version: CAPABILITY_VERSION,
+ affinity: "session",
+ hostPathAccess,
+ label: group.label,
+ description: group.description,
+ tools: Object.freeze(tools),
+ }),
+ );
+ }
+ return { offers: Object.freeze(offers), bindings };
}
function toolInputSchema(tool: MakaTool): Record<string, unknown> {
- const schema = toJSONSchema(requireZodSchema(tool), {
- io: "input",
- target: "draft-07",
- unrepresentable: "any",
- cycles: "ref",
- reused: "inline",
- });
- delete schema.$schema;
- if (schema.type !== "object") {
+ if (tool.parameters instanceof z.ZodType) {
+ const schema = toJSONSchema(tool.parameters, {
+ io: "input",
+ target: "draft-07",
+ unrepresentable: "any",
+ cycles: "ref",
+ reused: "inline",
+ });
+ delete schema.$schema;
+ if (schema.type !== "object") {
+ throw new Error(
+ `Desktop native capability tool schema must be an object: ${tool.name}`,
+ );
+ }
+ return Object.freeze(schema);
+ }
+
+ const wrapper = tool.parameters as JsonSchemaWrapper | undefined;
+ if (wrapper && typeof wrapper.jsonSchema === "object" && wrapper.jsonSchema) {
+ const schema = wrapper.jsonSchema;
+ if (typeof schema === "object" && schema !== null) {
+ return Object.freeze(
+ projectToolInputSchema(schema as Record<string, unknown>),
+ );
+ }
+ }
+
+ throw new Error(
+ `Desktop native capability tool has an unsupported schema type: ${tool.name}`,
+ );
+}
+
+interface JsonSchemaWrapper {
+ readonly jsonSchema?: Record<string, unknown>;
+}
+
+// -- JSON Schema argument validation -------------------------------------------
+//
+// Validation runs against the projected (advertised) schema, not the raw
+// MCP schema, so any constraint expressed via non-whitelisted keywords
+// (if/then/else, contains, dependentRequired, …) is dropped before Ajv
+// sees it. The downstream MCP server re-validates against the full schema.
+//
+// Regex constraints (pattern / patternProperties) are also omitted from the
+// local validator: the schema comes from an untrusted MCP server, and Ajv
+// executes those expressions synchronously on the call path, so a
+// pathological expression could block the Electron main process (ReDoS).
+// The MCP endpoint enforces them against the full schema.
+
+const jsonSchemaValidatorOptions = {
+ allErrors: true,
+ strict: false,
+ validateFormats: false,
+} as const;
+const schemaValidator = new Ajv2020(jsonSchemaValidatorOptions);
+const compiledSchemas = new WeakMap<object, ValidateFunction>();
+
+function adaptSchemaForLocalValidation(value: unknown): unknown {
+ if (value === null || typeof value !== 'object') return value;
+ if (Array.isArray(value)) return value.map((entry) => adaptSchemaForLocalValidation(entry));
+ const schema = value as Record<string, unknown>;
+ const result: Record<string, unknown> = {};
+ for (const [key, val] of Object.entries(schema)) {
+ if (key === 'pattern' || key === 'patternProperties') continue;
Review Comment:
**P2 (blocking).** Stripping `patternProperties` here while `additionalProperties` is passed through unchanged at line 586 makes the local validator reject valid input. For `{type:'object', patternProperties:{'^x-':{type:'string'}}, additionalProperties:false}`, the advertised schema accepts `{"x-foo":"bar"}` but local Ajv rejects it (`must NOT have additional properties`), so the call throws before reaching MCP. If you keep local validation, also relax `additionalProperties` to `true` whenever you drop `patternProperties`. If you delete local validation (recommended), this whole path goes away.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix: support MCP json schema tools [maka]
Posted by "liugddx (via GitHub)" <gi...@apache.org>.
liugddx commented on code in PR #4553:
URL: https://github.com/apache/maka/pull/4553#discussion_r3921656182
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -337,8 +339,7 @@ async function invokeNativeTool(
}
const signal = AbortSignal.any([options.signal, invocation.signal]);
signal.throwIfAborted();
- const parameters = requireZodSchema(binding.tool);
- const args = await parameters.parseAsync(frame.arguments);
+ const args = await parseNativeToolArguments(binding.tool.parameters, frame.arguments);
Review Comment:
P2 (validation bypass): parseNativeToolArguments -> validateTypes({value, schema: tool.parameters}). Verified against @ai-sdk/provider-utils@5.0.28: for an ai.jsonSchema(raw) wrapper built with no `validate` option, `validate` is undefined and safeValidateTypes returns the value unchanged — so MCP tool args reach impl() with NO enum/pattern/type/required enforcement. Repro: schema {type:object,properties:{mode:{enum:[read]}},required:[mode]} + args {mode:"DELETE_EVERYTHING",extra:1} pass through verbatim. The PR body's "parses through the declared schema shape" overclaims for exactly the jsonSchema tools this PR adds. Fix: compile a real validator (jsonSchema(raw,{validate})) or validate args against the projected schema before impl.
##########
packages/runtime-host/src/protocol/client-capability.ts:
##########
@@ -743,6 +743,7 @@ const CLIENT_CAPABILITY_SCHEMA_KEYWORDS = new Set([
'multipleOf',
'oneOf',
'pattern',
+ 'patternProperties',
Review Comment:
P1 (CI gate): this file is a protocol file, so the epoch guard (scripts/protocol-epoch-check.mjs, #3313) fails the `test` check while RUNTIME_HOST_COMPATIBILITY_EPOCH (protocol/index.ts:103) stays at the base's 98. Rebase onto current main and either bump the epoch past 98, or add a compatible-change declaration under packages/runtime-host/protocol-compatible-changes/ naming this file. Also: this is the authoritative keyword table + traversal — the desktop projection re-encodes the same structure, so keep them from drifting (see the note on the desktop switch).
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -448,29 +449,89 @@ function capabilityOffer(
}
function toolInputSchema(tool: MakaTool): Record<string, unknown> {
- const schema = toJSONSchema(requireZodSchema(tool), {
- io: "input",
- target: "draft-07",
- unrepresentable: "any",
- cycles: "ref",
- reused: "inline",
- });
- delete schema.$schema;
- if (schema.type !== "object") {
- throw new Error(
- `Desktop native capability tool schema must be an object: ${tool.name}`,
- );
+ if (tool.parameters instanceof z.ZodType) {
+ const schema = toJSONSchema(tool.parameters, {
+ io: "input",
+ target: "draft-07",
+ unrepresentable: "any",
+ cycles: "ref",
+ reused: "inline",
+ });
+ delete schema.$schema;
+ if (schema.type !== "object") {
+ throw new Error(
+ `Desktop native capability tool schema must be an object: ${tool.name}`,
+ );
+ }
+ return Object.freeze(schema);
}
- return Object.freeze(schema);
+
+ const wrapper = tool.parameters as JsonSchemaWrapper | undefined;
+ if (wrapper && typeof wrapper.jsonSchema === "object" && wrapper.jsonSchema) {
+ const schema = wrapper.jsonSchema;
+ if (typeof schema === "object" && schema !== null) {
+ return Object.freeze(projectClientCapabilitySchema(schema));
+ }
+ }
+
+ throw new Error(
+ `Desktop native capability tool has an unsupported schema type: ${tool.name}`,
+ );
+}
+
+interface JsonSchemaWrapper {
+ readonly jsonSchema?: Record<string, unknown>;
+}
+
+function projectClientCapabilitySchema(schema: Record<string, unknown>): Record<string, unknown> {
+ const result: Record<string, unknown> = {};
+ for (const [key, value] of Object.entries(schema)) {
+ if (!CLIENT_CAPABILITY_SCHEMA_KEYWORDS.has(key)) continue;
+ result[key] = projectClientCapabilitySchemaKeyword(key, value);
+ }
+ return result;
+}
+
+function projectClientCapabilitySchemaKeyword(key: string, value: unknown): unknown {
Review Comment:
P2 (second authority / duplication): this switch re-encodes the keyword->shape traversal that client-capability.ts:validateToolInputSchema.visit() already owns (record: properties/patternProperties/$defs/definitions; array: allOf/anyOf/oneOf; single-or-array: items; single: additionalProperties/propertyNames). This PR needed edits in three coupled sites for one keyword. Drift risk: add a future nested keyword to the shared set + protocol visit but forget this switch -> `default: return value` passes the subtree verbatim (incl. disallowed keywords) -> the protocol boundary rejects it -> the WHOLE registration fails to publish with an opaque error. Fix: export a single projectToolInputSchema() from @maka/runtime-host driven by the same shape table; desktop just picks which schema to hand in. (Also emits protocol-invalid schemas today: items:[] / empty composites -> boundary rejects the whole batch.)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix: support MCP json schema tools [maka]
Posted by "liugddx (via GitHub)" <gi...@apache.org>.
liugddx commented on code in PR #4553:
URL: https://github.com/apache/maka/pull/4553#discussion_r3924902408
##########
packages/runtime-host/src/protocol/client-capability.ts:
##########
@@ -743,6 +743,7 @@ const CLIENT_CAPABILITY_SCHEMA_KEYWORDS = new Set([
'multipleOf',
'oneOf',
'pattern',
+ 'patternProperties',
Review Comment:
P1 (CI gate): this file is a protocol file, so the epoch guard (scripts/protocol-epoch-check.mjs, #3313) fails the `test` check while RUNTIME_HOST_COMPATIBILITY_EPOCH (protocol/index.ts:103) stays at the base's 98. Rebase onto current main and either bump the epoch past 98, or add a compatible-change declaration under packages/runtime-host/protocol-compatible-changes/ naming this file. Also: this is the authoritative keyword table + traversal — the desktop projection re-encodes the same structure, so keep them from drifting (see the note on the desktop switch).
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -337,8 +339,7 @@ async function invokeNativeTool(
}
const signal = AbortSignal.any([options.signal, invocation.signal]);
signal.throwIfAborted();
- const parameters = requireZodSchema(binding.tool);
- const args = await parameters.parseAsync(frame.arguments);
+ const args = await parseNativeToolArguments(binding.tool.parameters, frame.arguments);
Review Comment:
P2 (validation bypass): parseNativeToolArguments -> validateTypes({value, schema: tool.parameters}). Verified against @ai-sdk/provider-utils@5.0.28: for an ai.jsonSchema(raw) wrapper built with no `validate` option, `validate` is undefined and safeValidateTypes returns the value unchanged — so MCP tool args reach impl() with NO enum/pattern/type/required enforcement. Repro: schema {type:object,properties:{mode:{enum:[read]}},required:[mode]} + args {mode:"DELETE_EVERYTHING",extra:1} pass through verbatim. The PR body's "parses through the declared schema shape" overclaims for exactly the jsonSchema tools this PR adds. Fix: compile a real validator (jsonSchema(raw,{validate})) or validate args against the projected schema before impl.
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -448,29 +449,89 @@ function capabilityOffer(
}
function toolInputSchema(tool: MakaTool): Record<string, unknown> {
- const schema = toJSONSchema(requireZodSchema(tool), {
- io: "input",
- target: "draft-07",
- unrepresentable: "any",
- cycles: "ref",
- reused: "inline",
- });
- delete schema.$schema;
- if (schema.type !== "object") {
- throw new Error(
- `Desktop native capability tool schema must be an object: ${tool.name}`,
- );
+ if (tool.parameters instanceof z.ZodType) {
+ const schema = toJSONSchema(tool.parameters, {
+ io: "input",
+ target: "draft-07",
+ unrepresentable: "any",
+ cycles: "ref",
+ reused: "inline",
+ });
+ delete schema.$schema;
+ if (schema.type !== "object") {
+ throw new Error(
+ `Desktop native capability tool schema must be an object: ${tool.name}`,
+ );
+ }
+ return Object.freeze(schema);
}
- return Object.freeze(schema);
+
+ const wrapper = tool.parameters as JsonSchemaWrapper | undefined;
+ if (wrapper && typeof wrapper.jsonSchema === "object" && wrapper.jsonSchema) {
+ const schema = wrapper.jsonSchema;
+ if (typeof schema === "object" && schema !== null) {
+ return Object.freeze(projectClientCapabilitySchema(schema));
+ }
+ }
+
+ throw new Error(
+ `Desktop native capability tool has an unsupported schema type: ${tool.name}`,
+ );
+}
+
+interface JsonSchemaWrapper {
+ readonly jsonSchema?: Record<string, unknown>;
+}
+
+function projectClientCapabilitySchema(schema: Record<string, unknown>): Record<string, unknown> {
+ const result: Record<string, unknown> = {};
+ for (const [key, value] of Object.entries(schema)) {
+ if (!CLIENT_CAPABILITY_SCHEMA_KEYWORDS.has(key)) continue;
+ result[key] = projectClientCapabilitySchemaKeyword(key, value);
+ }
+ return result;
+}
+
+function projectClientCapabilitySchemaKeyword(key: string, value: unknown): unknown {
Review Comment:
P2 (second authority / duplication): this switch re-encodes the keyword->shape traversal that client-capability.ts:validateToolInputSchema.visit() already owns (record: properties/patternProperties/$defs/definitions; array: allOf/anyOf/oneOf; single-or-array: items; single: additionalProperties/propertyNames). This PR needed edits in three coupled sites for one keyword. Drift risk: add a future nested keyword to the shared set + protocol visit but forget this switch -> `default: return value` passes the subtree verbatim (incl. disallowed keywords) -> the protocol boundary rejects it -> the WHOLE registration fails to publish with an opaque error. Fix: export a single projectToolInputSchema() from @maka/runtime-host driven by the same shape table; desktop just picks which schema to hand in. (Also emits protocol-invalid schemas today: items:[] / empty composites -> boundary rejects the whole batch.)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix: support MCP json schema tools [maka]
Posted by "hqhq1025 (via GitHub)" <gi...@apache.org>.
hqhq1025 commented on code in PR #4553:
URL: https://github.com/apache/maka/pull/4553#discussion_r3926862979
##########
packages/runtime-host/src/protocol/index.ts:
##########
@@ -100,7 +100,11 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1 as const;
export const RUNTIME_HOST_PROTOCOL_VERSION = 0 as const;
// Increment when the same protocol version no longer guarantees safe Client-Host
// interoperability. Mismatches are rejected before domain commands are admitted.
-export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 105 as const;
+export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 106 as const;
Review Comment:
**P1 - rebase and allocate a fresh compatibility epoch before this can merge.** Current `main` (`486e9ba65536d45cd54e7f6b471c205a217ecc94`) already declares epoch 109 for other incompatible protocol changes, while this head declares 106 for the new Client Capability schema vocabulary. `git merge-tree` reports a content conflict in this file. After resolving that conflict, keeping main’s 109 would still fail `scripts/protocol-epoch-check.mjs` because `client-capability.ts` changes without a compatible-change declaration; keeping 106 would move the epoch backward. Rebase onto current `main`, preserve its 106-109 history, and assign the next unused epoch (currently 110) to this incompatible change.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix: support MCP json schema tools [maka]
Posted by "liugddx (via GitHub)" <gi...@apache.org>.
liugddx commented on code in PR #4553:
URL: https://github.com/apache/maka/pull/4553#discussion_r3924878975
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -337,8 +339,7 @@ async function invokeNativeTool(
}
const signal = AbortSignal.any([options.signal, invocation.signal]);
signal.throwIfAborted();
- const parameters = requireZodSchema(binding.tool);
- const args = await parameters.parseAsync(frame.arguments);
+ const args = await parseNativeToolArguments(binding.tool.parameters, frame.arguments);
Review Comment:
P2 (validation bypass): parseNativeToolArguments -> validateTypes({value, schema: tool.parameters}). Verified against @ai-sdk/provider-utils@5.0.28: for an ai.jsonSchema(raw) wrapper built with no `validate` option, `validate` is undefined and safeValidateTypes returns the value unchanged — so MCP tool args reach impl() with NO enum/pattern/type/required enforcement. Repro: schema {type:object,properties:{mode:{enum:[read]}},required:[mode]} + args {mode:"DELETE_EVERYTHING",extra:1} pass through verbatim. The PR body's "parses through the declared schema shape" overclaims for exactly the jsonSchema tools this PR adds. Fix: compile a real validator (jsonSchema(raw,{validate})) or validate args against the projected schema before impl.
##########
packages/runtime-host/src/protocol/client-capability.ts:
##########
@@ -743,6 +743,7 @@ const CLIENT_CAPABILITY_SCHEMA_KEYWORDS = new Set([
'multipleOf',
'oneOf',
'pattern',
+ 'patternProperties',
Review Comment:
P1 (CI gate): this file is a protocol file, so the epoch guard (scripts/protocol-epoch-check.mjs, #3313) fails the `test` check while RUNTIME_HOST_COMPATIBILITY_EPOCH (protocol/index.ts:103) stays at the base's 98. Rebase onto current main and either bump the epoch past 98, or add a compatible-change declaration under packages/runtime-host/protocol-compatible-changes/ naming this file. Also: this is the authoritative keyword table + traversal — the desktop projection re-encodes the same structure, so keep them from drifting (see the note on the desktop switch).
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -448,29 +449,89 @@ function capabilityOffer(
}
function toolInputSchema(tool: MakaTool): Record<string, unknown> {
- const schema = toJSONSchema(requireZodSchema(tool), {
- io: "input",
- target: "draft-07",
- unrepresentable: "any",
- cycles: "ref",
- reused: "inline",
- });
- delete schema.$schema;
- if (schema.type !== "object") {
- throw new Error(
- `Desktop native capability tool schema must be an object: ${tool.name}`,
- );
+ if (tool.parameters instanceof z.ZodType) {
+ const schema = toJSONSchema(tool.parameters, {
+ io: "input",
+ target: "draft-07",
+ unrepresentable: "any",
+ cycles: "ref",
+ reused: "inline",
+ });
+ delete schema.$schema;
+ if (schema.type !== "object") {
+ throw new Error(
+ `Desktop native capability tool schema must be an object: ${tool.name}`,
+ );
+ }
+ return Object.freeze(schema);
}
- return Object.freeze(schema);
+
+ const wrapper = tool.parameters as JsonSchemaWrapper | undefined;
+ if (wrapper && typeof wrapper.jsonSchema === "object" && wrapper.jsonSchema) {
+ const schema = wrapper.jsonSchema;
+ if (typeof schema === "object" && schema !== null) {
+ return Object.freeze(projectClientCapabilitySchema(schema));
+ }
+ }
+
+ throw new Error(
+ `Desktop native capability tool has an unsupported schema type: ${tool.name}`,
+ );
+}
+
+interface JsonSchemaWrapper {
+ readonly jsonSchema?: Record<string, unknown>;
+}
+
+function projectClientCapabilitySchema(schema: Record<string, unknown>): Record<string, unknown> {
+ const result: Record<string, unknown> = {};
+ for (const [key, value] of Object.entries(schema)) {
+ if (!CLIENT_CAPABILITY_SCHEMA_KEYWORDS.has(key)) continue;
+ result[key] = projectClientCapabilitySchemaKeyword(key, value);
+ }
+ return result;
+}
+
+function projectClientCapabilitySchemaKeyword(key: string, value: unknown): unknown {
Review Comment:
P2 (second authority / duplication): this switch re-encodes the keyword->shape traversal that client-capability.ts:validateToolInputSchema.visit() already owns (record: properties/patternProperties/$defs/definitions; array: allOf/anyOf/oneOf; single-or-array: items; single: additionalProperties/propertyNames). This PR needed edits in three coupled sites for one keyword. Drift risk: add a future nested keyword to the shared set + protocol visit but forget this switch -> `default: return value` passes the subtree verbatim (incl. disallowed keywords) -> the protocol boundary rejects it -> the WHOLE registration fails to publish with an opaque error. Fix: export a single projectToolInputSchema() from @maka/runtime-host driven by the same shape table; desktop just picks which schema to hand in. (Also emits protocol-invalid schemas today: items:[] / empty composites -> boundary rejects the whole batch.)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix: support MCP json schema tools [maka]
Posted by "Shkin1 (via GitHub)" <gi...@apache.org>.
Shkin1 commented on PR #4553:
URL: https://github.com/apache/maka/pull/4553#issuecomment-5525051123
> Reviewed exact head `d46ca239e33e5cbd3db65ca72044b0d80a65b0e9` against current `main` `148f8eb297c86aa3045c75e87e19cacd4967c2dc`.
>
> The diff replaces the Zod-only Desktop capability path with shared JSON-Schema projection, adds `patternProperties`, validates MCP arguments with cached Ajv 2020 validators, and advances the Runtime Host compatibility epoch to 105. The happy path and the earlier validation/cache issues are fixed, but this head still has one P1 and two P2 correctness/availability findings in the inline comments below.
>
> Validation performed:
>
> * Clean `npm ci`, `npm run build:test`, and full workspace `npm run typecheck` passed on the exact head.
> * Changed-file Biome, `git diff --check`, and protocol epoch guard (`104 -> 105`) passed.
> * Focused Desktop/Runtime Host capability suites passed 28/28 on the exact head and on a conflict-free synthetic merge onto current main; synthetic tree: `db3942437fb458eff17333dcb26a84d3c52cac90`.
> * Full Runtime Host: 1652 passed, 1 failed, 12 skipped. The sole failure is in unchanged managed-sandbox coverage and reproduced on the synthetic merge; this runner rejects both `unshare` and `bwrap` with permission denied.
> * Full Desktop: 2027 passed, 0 failed, 8 cancelled in the unchanged MCP OAuth deadline group.
> * All hosted checks are terminal green at publication.
>
> Not independently exercised: native Windows/macOS packaging/runtime behavior or a third-party MCP server outside deterministic local provider probes.
>
> > **Automated review notice:** This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
The three findings above are addressed in b90aeb548: regex constraints are stripped from the local validator (ReDoS), tuple items are translated to prefixItems for Ajv 2020, and malformed MCP tools are skipped per-tool so the rest of the registration survives. Focused suites pass 23/23 (desktop) and 5/5 (runtime-host). Re-reviewed head: b90aeb548.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix: support MCP json schema tools [maka]
Posted by "Shkin1 (via GitHub)" <gi...@apache.org>.
Shkin1 commented on PR #4553:
URL: https://github.com/apache/maka/pull/4553#issuecomment-5540917761
> Follow-up review on the unchanged head `0ca1d905c1c1f0d8a21de384e05215ebf0e02733`.
>
> The previous E2E positional failure is fixed, but the core blocking findings remain unresolved:
>
> **P1 — Untrusted MCP regexes still execute synchronously on the Runtime main thread.** `validateJsonSchemaInput()` invokes Ajv synchronously for schemas supplied by an MCP server. A pattern such as `^(a+)+$` with a long non-matching argument can block the event loop before cancellation is observed. This needs a linear-time regex engine, worker/deadline isolation, or server-only validation.
>
> **P2 — Desktop still uses a second schema projector.** `declaredToolInputSchema()` continues to call the local `projectClientCapabilitySchema()` even though the Runtime Host projector is imported. The two implementations diverge for malformed nested containers such as `items: []`, causing valid dynamic MCP registration to omit tools after protocol decoding. Use `projectToolInputSchema()` directly and delete the Desktop copy.
>
> **P2 — The model-visible schema and the locally enforced schema still differ.** Publication drops constraints such as `if`, `then`, `dependentSchemas`, and `prefixItems`, while Runtime validates the full schema. The model can therefore generate an argument that the published schema accepts but the local validator rejects before the MCP server sees it. Align the advertised and enforced contracts, or make the MCP server the sole JSON Schema authority.
>
> **P2 — Draft detection still scans arbitrary instance data.** `hasDraft7TupleItems()` treats `items` arrays inside `const`, `default`, or `examples` as Draft-07 tuples and can override an explicit Draft-2020 dialect. Restrict the scan to actual schema nodes and always honour an explicit `$schema`.
>
> The candidate and protocol negative tests remain non-causal: the candidate test still exercises the old fixed Browser failure path, and the invalid `patternProperties` case can pass because the keyword was unsupported before this change. Add tests that invoke the newly accepted `annotated` MCP tool, exercise dynamic-tool isolation, isolate invalid-regex validation from unsupported-keyword rejection, and cover explicit Draft-07/2019-09/2020-12 dialects.
>
> The earlier validation bypass and stale positional E2E findings are resolved. I am keeping `REQUEST_CHANGES` until the four findings above are addressed.
@liugddx
Thanks for the detailed follow-up review. Yes, I understand that the previous E2E positional failure and validation bypass issues are resolved, but the latest head still has the four remaining blocking issues you listed.
I’ll address them together:
- remove synchronous validation of untrusted MCP regexes from the Runtime main-thread path, with the MCP server remaining the JSON Schema authority;
- make `projectToolInputSchema()` the only schema projection authority and remove the Desktop duplicate;
- ensure the advertised schema and the locally enforced behavior cannot diverge;
- make explicit `$schema` dialects authoritative and restrict tuple detection to actual schema nodes only.
I’ll also replace the non-causal tests with regression tests that:
- invoke the accepted `annotated` MCP tool;
- prove malformed dynamic MCP tools are isolated while healthy tools remain available;
- validate invalid `patternProperties` regexes independently of unsupported-keyword handling;
- cover explicit Draft-07, Draft-2019-09, and Draft-2020-12 dialects.
I’ll rebase onto the latest `main`, verify the current protocol compatibility epoch and merge state, run the complete relevant test surface, and then push an update. I’ll follow up with the exact commit and verification results once all four findings are addressed.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix: support MCP json schema tools [maka]
Posted by "Shkin1 (via GitHub)" <gi...@apache.org>.
Shkin1 commented on PR #4553:
URL: https://github.com/apache/maka/pull/4553#issuecomment-5535814687
Thanks everyone for the detailed reviews and follow-up feedback.
I have updated the implementation to fully support MCP tools using the `ai.jsonSchema()` wrapper in Desktop.
The main changes include:
- Runtime now creates MCP proxy tools with Runtime-owned JSON Schema validation.
- Desktop supports both Zod schemas and `ai.jsonSchema()` wrappers during native tool invocation.
- MCP tool schemas are projected through the Runtime Host Client Capability vocabulary before publication.
- Added support for `patternProperties`, nested schemas, tuple schemas, Draft-07, Draft-2019-09, and Draft-2020-12 behavior.
- Preserved supported annotations such as `default`, `enum`, and `examples`.
- Removed protocol-disallowed fields such as `$schema` and `$id` from published capability schemas.
- Malformed MCP tools are isolated individually and reported through the Desktop Runtime Host diagnostic path.
- First-party capabilities such as Browser and Computer Use still fail fast when their schemas are invalid.
- Preserved the dynamic MCP grouping, identity-aware tool publication, chunking, and manifest budget behavior from the latest upstream changes.
- Updated the Runtime Host protocol compatibility epoch from 109 to 110 because the accepted Client Capability schema vocabulary changed.
I also rebased the branch onto the latest `upstream/main` and resolved the conflicts, including the changes in `runtime-host-native-capabilities.ts`. The current branch has no unresolved merge conflicts.
Local verification completed successfully:
- `npm run build:test`
- `npm run typecheck`
- `npm run lint`
- `npm run format:check`
- Desktop capability tests: 51 passed
- Runtime MCP tests: 12 passed
- Runtime Host protocol tests: 6 passed
- `git diff --check`
- Final merge-tree conflict check against the latest `upstream/main`
The latest commit has been pushed to the PR branch.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix: support MCP json schema tools [maka]
Posted by "liugddx (via GitHub)" <gi...@apache.org>.
liugddx commented on code in PR #4553:
URL: https://github.com/apache/maka/pull/4553#discussion_r3922189101
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -448,29 +452,152 @@ function capabilityOffer(
}
function toolInputSchema(tool: MakaTool): Record<string, unknown> {
- const schema = toJSONSchema(requireZodSchema(tool), {
- io: "input",
- target: "draft-07",
- unrepresentable: "any",
- cycles: "ref",
- reused: "inline",
- });
- delete schema.$schema;
- if (schema.type !== "object") {
- throw new Error(
- `Desktop native capability tool schema must be an object: ${tool.name}`,
- );
+ if (tool.parameters instanceof z.ZodType) {
+ const schema = toJSONSchema(tool.parameters, {
+ io: "input",
+ target: "draft-07",
+ unrepresentable: "any",
+ cycles: "ref",
+ reused: "inline",
+ });
+ delete schema.$schema;
+ if (schema.type !== "object") {
+ throw new Error(
+ `Desktop native capability tool schema must be an object: ${tool.name}`,
+ );
+ }
+ return Object.freeze(schema);
}
- return Object.freeze(schema);
+
+ const wrapper = tool.parameters as JsonSchemaWrapper | undefined;
+ if (wrapper && typeof wrapper.jsonSchema === "object" && wrapper.jsonSchema) {
+ const schema = wrapper.jsonSchema;
+ if (typeof schema === "object" && schema !== null) {
+ return Object.freeze(
+ projectToolInputSchema(schema as Record<string, unknown>),
+ );
+ }
+ }
+
+ throw new Error(
+ `Desktop native capability tool has an unsupported schema type: ${tool.name}`,
+ );
}
-function requireZodSchema(tool: MakaTool): z.ZodType {
- if (!(tool.parameters instanceof z.ZodType)) {
+interface JsonSchemaWrapper {
+ readonly jsonSchema?: Record<string, unknown>;
+}
+
+// -- JSON Schema argument validation -------------------------------------------
+
+const jsonSchemaValidatorOptions = {
+ allErrors: true,
+ strict: false,
+ validateFormats: false,
+} as const;
+const draft7Validator = new Ajv(jsonSchemaValidatorOptions);
+const draft2019Validator = new Ajv2019(jsonSchemaValidatorOptions);
+const draft2020Validator = new Ajv2020(jsonSchemaValidatorOptions);
+const compiledSchemas = new WeakMap<object, ValidateFunction>();
+
+function compileJsonSchema(schema: unknown): ValidateFunction | undefined {
+ if (typeof schema === 'boolean') return draft2020Validator.compile(schema);
+ if (typeof schema !== 'object' || schema === null || Array.isArray(schema))
+ return undefined;
+ const cached = compiledSchemas.get(schema);
+ if (cached) return cached;
+ const declaredDialect = (
+ schema as { readonly $schema?: unknown }
+ ).$schema;
+ const dialect =
+ typeof declaredDialect === 'string' ? declaredDialect : '';
+ const validator = dialect.includes('draft-07')
+ ? draft7Validator
+ : dialect.includes('2019-09')
+ ? draft2019Validator
+ : draft2020Validator;
+ const compiled = validator.compile(schema as AnySchema);
+ compiledSchemas.set(schema, compiled);
+ return compiled;
+}
+
+async function parseNativeToolArguments(
+ parameters: unknown,
+ args: unknown,
+): Promise<unknown> {
+ if (
+ !parameters ||
+ (typeof parameters !== 'object' && typeof parameters !== 'function')
+ ) {
+ return args;
+ }
+
+ // Zod schemas: the provider-utils path parses correctly.
+ if (parameters instanceof z.ZodType) {
+ return await validateTypes({ value: args, schema: parameters as never });
+ }
+
+ // jsonSchema() wrappers: compile the projected schema and validate.
+ const wrapper = parameters as JsonSchemaWrapper | undefined;
+ if (wrapper?.jsonSchema) {
+ const projected = projectToolInputSchema(
+ wrapper.jsonSchema as Record<string, unknown>,
+ );
+ const validator = compileJsonSchema(projected);
Review Comment:
P2 (efficiency): the Ajv compile cache never hits. `projectToolInputSchema(wrapper.jsonSchema)` returns a FRESH object every invocation, and `compiledSchemas` (WeakMap) is keyed on that ephemeral `projected` object — so `compiledSchemas.get(projected)` always misses and `validator.compile()` (the expensive step) runs on every native tool call. Fix: key the cache on the stable `wrapper.jsonSchema` (or the binding), or project+compile once when the offer is built and reuse the ValidateFunction.
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -448,29 +452,152 @@ function capabilityOffer(
}
function toolInputSchema(tool: MakaTool): Record<string, unknown> {
- const schema = toJSONSchema(requireZodSchema(tool), {
- io: "input",
- target: "draft-07",
- unrepresentable: "any",
- cycles: "ref",
- reused: "inline",
- });
- delete schema.$schema;
- if (schema.type !== "object") {
- throw new Error(
- `Desktop native capability tool schema must be an object: ${tool.name}`,
- );
+ if (tool.parameters instanceof z.ZodType) {
+ const schema = toJSONSchema(tool.parameters, {
+ io: "input",
+ target: "draft-07",
+ unrepresentable: "any",
+ cycles: "ref",
+ reused: "inline",
+ });
+ delete schema.$schema;
+ if (schema.type !== "object") {
+ throw new Error(
+ `Desktop native capability tool schema must be an object: ${tool.name}`,
+ );
+ }
+ return Object.freeze(schema);
}
- return Object.freeze(schema);
+
+ const wrapper = tool.parameters as JsonSchemaWrapper | undefined;
+ if (wrapper && typeof wrapper.jsonSchema === "object" && wrapper.jsonSchema) {
+ const schema = wrapper.jsonSchema;
+ if (typeof schema === "object" && schema !== null) {
+ return Object.freeze(
+ projectToolInputSchema(schema as Record<string, unknown>),
+ );
+ }
+ }
+
+ throw new Error(
+ `Desktop native capability tool has an unsupported schema type: ${tool.name}`,
+ );
}
-function requireZodSchema(tool: MakaTool): z.ZodType {
- if (!(tool.parameters instanceof z.ZodType)) {
+interface JsonSchemaWrapper {
+ readonly jsonSchema?: Record<string, unknown>;
+}
+
+// -- JSON Schema argument validation -------------------------------------------
+
+const jsonSchemaValidatorOptions = {
+ allErrors: true,
+ strict: false,
+ validateFormats: false,
+} as const;
+const draft7Validator = new Ajv(jsonSchemaValidatorOptions);
+const draft2019Validator = new Ajv2019(jsonSchemaValidatorOptions);
+const draft2020Validator = new Ajv2020(jsonSchemaValidatorOptions);
+const compiledSchemas = new WeakMap<object, ValidateFunction>();
+
+function compileJsonSchema(schema: unknown): ValidateFunction | undefined {
+ if (typeof schema === 'boolean') return draft2020Validator.compile(schema);
+ if (typeof schema !== 'object' || schema === null || Array.isArray(schema))
+ return undefined;
+ const cached = compiledSchemas.get(schema);
+ if (cached) return cached;
+ const declaredDialect = (
+ schema as { readonly $schema?: unknown }
+ ).$schema;
+ const dialect =
+ typeof declaredDialect === 'string' ? declaredDialect : '';
+ const validator = dialect.includes('draft-07')
Review Comment:
P3 (Occam + edge): this dialect dispatch is dead code. `$schema` is not in CLIENT_CAPABILITY_SCHEMA_KEYWORDS, so `projectToolInputSchema` strips it; `compileJsonSchema` then always sees no `$schema` -> dialect '' -> always `draft2020Validator`. `draft7Validator`/`draft2019Validator` and the draft-07/2019-09 branches are unreachable for the projected path. Either delete the two unused validators + dispatch, or compile the RAW schema (which still carries `$schema`) so the dispatch is meaningful. Sub-edge worth a test: a draft-07 tuple `items:[A,B]` survives projection but Ajv2020 treats `items` as a single schema (tuple moved to `prefixItems`) -> mis-validated or a compile throw.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix: support MCP json schema tools [maka]
Posted by "Astro-Han (via GitHub)" <gi...@apache.org>.
Astro-Han merged PR #4553:
URL: https://github.com/apache/maka/pull/4553
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix: support MCP json schema tools [maka]
Posted by "Shkin1 (via GitHub)" <gi...@apache.org>.
Shkin1 commented on PR #4553:
URL: https://github.com/apache/maka/pull/4553#issuecomment-5536015914
Thanks everyone for the detailed reviews and follow-up feedback.
I have updated the implementation to fully support MCP tools using the `ai.jsonSchema()` wrapper in Desktop.
The main changes include:
- Runtime now creates MCP proxy tools with Runtime-owned JSON Schema validation.
- Desktop supports both Zod schemas and `ai.jsonSchema()` wrappers during native tool invocation.
- MCP tool schemas are projected through the Runtime Host Client Capability vocabulary before publication.
- Added support for `patternProperties`, nested schemas, tuple schemas, Draft-07, Draft-2019-09, and Draft-2020-12 behavior.
- Preserved supported annotations such as `default`, `enum`, and `examples`.
- Removed protocol-disallowed fields such as `$schema` and `$id` from published capability schemas.
- Malformed MCP tools are isolated individually and reported through the Desktop Runtime Host diagnostic path.
- First-party capabilities such as Browser and Computer Use still fail fast when their schemas are invalid.
- Preserved the dynamic MCP grouping, identity-aware tool publication, chunking, and manifest budget behavior from the latest upstream changes.
- Updated the Runtime Host protocol compatibility epoch from 109 to 110 because the accepted Client Capability schema vocabulary changed.
I also rebased the branch onto the latest `upstream/main` and resolved the conflicts, including the changes in `runtime-host-native-capabilities.ts`. The current branch has no unresolved merge conflicts.
Local verification completed successfully:
- `npm run build:test`
- `npm run typecheck`
- `npm run lint`
- `npm run format:check`
- Desktop capability tests: 51 passed
- Runtime MCP tests: 12 passed
- Runtime Host protocol tests: 6 passed
- `git diff --check`
- Final merge-tree conflict check against the latest `upstream/main`
The latest commit has been pushed to the PR branch.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix: support MCP json schema tools [maka]
Posted by "hqhq1025 (via GitHub)" <gi...@apache.org>.
hqhq1025 commented on code in PR #4553:
URL: https://github.com/apache/maka/pull/4553#discussion_r3923235221
##########
packages/runtime-host/src/protocol/client-capability.ts:
##########
@@ -751,6 +752,84 @@ const CLIENT_CAPABILITY_SCHEMA_KEYWORDS = new Set([
'uniqueItems',
]);
+const CLIENT_CAPABILITY_SCHEMA_CONTAINER_SHAPES: Record<
+ string,
+ 'record' | 'array' | 'single_or_array' | 'single'
+> = {
+ properties: 'record',
+ patternProperties: 'record',
+ $defs: 'record',
+ definitions: 'record',
+ allOf: 'array',
+ anyOf: 'array',
+ oneOf: 'array',
+ items: 'single_or_array',
+ additionalProperties: 'single',
+ propertyNames: 'single',
+};
+
+/**
+ * Project an external JSON Schema (e.g. from an MCP tool) down to exactly the
+ * keywords the Client Capability protocol admits, recursing into nested schemas
+ * via the same shape table that {@link validateToolInputSchema} uses.
+ *
+ * `$ref` is retained when it resolves locally inside `$defs`/`definitions`;
+ * otherwise upstream callers should omit it first.
+ *
+ * Empty `items`, `allOf`, `anyOf`, and `oneOf` are dropped so the projected
+ * schema never emits a shape the protocol boundary rejects.
+ */
+export function projectToolInputSchema(schema: Record<string, unknown>): Record<string, unknown> {
+ if (!Object.hasOwn(schema, 'type') || schema.type !== 'object') {
+ throw new Error('Client Capability tool schema root must be an object');
+ }
+ return projectSchemaNode(schema) as Record<string, unknown>;
+}
+
+function projectSchemaNode(value: unknown): unknown {
+ if (value === null || typeof value !== 'object') return value;
+ if (Array.isArray(value)) return value.map((entry) => projectSchemaNode(entry));
+ const schema = value as Record<string, unknown>;
+ const result: Record<string, unknown> = {};
+ for (const [key, val] of Object.entries(schema)) {
+ if (!CLIENT_CAPABILITY_SCHEMA_KEYWORDS.has(key)) continue;
+ const projected = projectSchemaKeyword(key, val);
+ if (projected !== undefined) {
+ result[key] = projected;
+ }
+ }
+ return result;
+}
+
+function projectSchemaKeyword(key: string, value: unknown): unknown {
+ const shape = CLIENT_CAPABILITY_SCHEMA_CONTAINER_SHAPES[key];
+ if (shape === undefined) return value;
+ switch (shape) {
+ case 'record': {
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return {};
+ const result: Record<string, unknown> = {};
+ for (const [nestedKey, nestedValue] of Object.entries(value as Record<string, unknown>)) {
+ result[nestedKey] = projectSchemaNode(nestedValue);
+ }
+ return result;
+ }
+ case 'array': {
+ if (!Array.isArray(value) || value.length === 0) return undefined;
+ return value.map((entry) => projectSchemaNode(entry));
+ }
+ case 'single_or_array': {
+ if (Array.isArray(value)) {
+ if (value.length === 0) return undefined;
+ return value.map((entry) => projectSchemaNode(entry));
Review Comment:
**P2 - the protocol advertises schemas that the invocation validator cannot compile.** `items` arrays are deliberately retained here and the new protocol test accepts `items: [{type:"integer"}, {type:"integer"}]`, but Desktop always compiles the projected schema with Ajv 2020. The MCP layer and this projection both remove `$schema`, so an otherwise valid draft-07 tuple loses its dialect. Exact-head probe: publication passed `decodeClientCapabilityReplaceInput`, then a valid `{coordinate:[1,2]}` call failed before `impl` with `items must be object,boolean`. The same mismatch exists for regexes accepted by `new RegExp(pattern)` but rejected by Ajv unicode mode, e.g. `\\8`. Make the protocol vocabulary match the selected validator, or preserve/translate the source dialect before advertising the schema.
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -448,29 +449,127 @@ function capabilityOffer(
}
function toolInputSchema(tool: MakaTool): Record<string, unknown> {
- const schema = toJSONSchema(requireZodSchema(tool), {
- io: "input",
- target: "draft-07",
- unrepresentable: "any",
- cycles: "ref",
- reused: "inline",
- });
- delete schema.$schema;
- if (schema.type !== "object") {
+ if (tool.parameters instanceof z.ZodType) {
+ const schema = toJSONSchema(tool.parameters, {
+ io: "input",
+ target: "draft-07",
+ unrepresentable: "any",
+ cycles: "ref",
+ reused: "inline",
+ });
+ delete schema.$schema;
+ if (schema.type !== "object") {
+ throw new Error(
+ `Desktop native capability tool schema must be an object: ${tool.name}`,
+ );
+ }
+ return Object.freeze(schema);
+ }
+
+ const wrapper = tool.parameters as JsonSchemaWrapper | undefined;
+ if (wrapper && typeof wrapper.jsonSchema === "object" && wrapper.jsonSchema) {
+ const schema = wrapper.jsonSchema;
+ if (typeof schema === "object" && schema !== null) {
+ return Object.freeze(
+ projectToolInputSchema(schema as Record<string, unknown>),
+ );
+ }
+ }
+
+ throw new Error(
+ `Desktop native capability tool has an unsupported schema type: ${tool.name}`,
+ );
+}
+
+interface JsonSchemaWrapper {
+ readonly jsonSchema?: Record<string, unknown>;
+}
+
+// -- JSON Schema argument validation -------------------------------------------
+//
+// Validation runs against the projected (advertised) schema, not the raw
+// MCP schema, so any constraint expressed via non-whitelisted keywords
+// (if/then/else, contains, dependentRequired, …) is dropped before Ajv
+// sees it. The downstream MCP server re-validates against the full schema.
+
+const jsonSchemaValidatorOptions = {
+ allErrors: true,
+ strict: false,
+ validateFormats: false,
+} as const;
+const schemaValidator = new Ajv2020(jsonSchemaValidatorOptions);
Review Comment:
**P1 - untrusted MCP regexes can synchronously block the Electron main process.** This Ajv instance compiles and executes the projected `pattern`/`patternProperties` expressions on the call path before admission, with no timeout or isolation. The schema comes from the connected MCP server, while Client Capability arguments may be up to 40 KiB. On this exact head, the valid pattern `^(a+)+$` took about 1.8 seconds to reject only 29 input characters; the cost grows exponentially, and the synchronous validator cannot observe the abort signal while it is running. A remote/untrusted MCP descriptor can therefore freeze all Desktop main-process work when its tool is invoked. Use a linear-time regex engine or worker/deadline isolation, or omit regex constraints from local validation and let the MCP endpoint enforce them.
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -448,29 +449,127 @@ function capabilityOffer(
}
function toolInputSchema(tool: MakaTool): Record<string, unknown> {
- const schema = toJSONSchema(requireZodSchema(tool), {
- io: "input",
- target: "draft-07",
- unrepresentable: "any",
- cycles: "ref",
- reused: "inline",
- });
- delete schema.$schema;
- if (schema.type !== "object") {
+ if (tool.parameters instanceof z.ZodType) {
+ const schema = toJSONSchema(tool.parameters, {
+ io: "input",
+ target: "draft-07",
+ unrepresentable: "any",
+ cycles: "ref",
+ reused: "inline",
+ });
+ delete schema.$schema;
+ if (schema.type !== "object") {
+ throw new Error(
+ `Desktop native capability tool schema must be an object: ${tool.name}`,
+ );
+ }
+ return Object.freeze(schema);
+ }
+
+ const wrapper = tool.parameters as JsonSchemaWrapper | undefined;
+ if (wrapper && typeof wrapper.jsonSchema === "object" && wrapper.jsonSchema) {
+ const schema = wrapper.jsonSchema;
+ if (typeof schema === "object" && schema !== null) {
+ return Object.freeze(
+ projectToolInputSchema(schema as Record<string, unknown>),
Review Comment:
**P2 - one malformed MCP schema still unregisters every Desktop capability.** This path projects each schema but does not contain a per-tool failure. The new invalid-`patternProperties` test proves the provider can be constructed and only fails when the complete offer set is canonicalized. Production combines Browser, Computer Use, settings, Rive, and every MCP tool in one provider; `snapshotProvider()` decodes that whole set, and `refresh()` catches any schema error by calling `#clearRegistration()`. Exact-head probe: after publishing `desktop_browser`, `desktop_settings`, and `desktop_mcp`, refreshing with one MCP key `"("` produced `CapabilityProviderPublicationError` and one `unregisterClientCapabilities` call. Validate and omit/report only the bad MCP descriptor, or isolate MCP publication so unrelated local capabilities remain registered.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix: support MCP json schema tools [maka]
Posted by "liugddx (via GitHub)" <gi...@apache.org>.
liugddx commented on code in PR #4553:
URL: https://github.com/apache/maka/pull/4553#discussion_r3924928400
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -337,8 +339,7 @@ async function invokeNativeTool(
}
const signal = AbortSignal.any([options.signal, invocation.signal]);
signal.throwIfAborted();
- const parameters = requireZodSchema(binding.tool);
- const args = await parameters.parseAsync(frame.arguments);
+ const args = await parseNativeToolArguments(binding.tool.parameters, frame.arguments);
Review Comment:
P2 (validation bypass): parseNativeToolArguments -> validateTypes({value, schema: tool.parameters}). Verified against @ai-sdk/provider-utils@5.0.28: for an ai.jsonSchema(raw) wrapper built with no `validate` option, `validate` is undefined and safeValidateTypes returns the value unchanged — so MCP tool args reach impl() with NO enum/pattern/type/required enforcement. Repro: schema {type:object,properties:{mode:{enum:[read]}},required:[mode]} + args {mode:"DELETE_EVERYTHING",extra:1} pass through verbatim. The PR body's "parses through the declared schema shape" overclaims for exactly the jsonSchema tools this PR adds. Fix: compile a real validator (jsonSchema(raw,{validate})) or validate args against the projected schema before impl.
##########
packages/runtime-host/src/protocol/client-capability.ts:
##########
@@ -743,6 +743,7 @@ const CLIENT_CAPABILITY_SCHEMA_KEYWORDS = new Set([
'multipleOf',
'oneOf',
'pattern',
+ 'patternProperties',
Review Comment:
P1 (CI gate): this file is a protocol file, so the epoch guard (scripts/protocol-epoch-check.mjs, #3313) fails the `test` check while RUNTIME_HOST_COMPATIBILITY_EPOCH (protocol/index.ts:103) stays at the base's 98. Rebase onto current main and either bump the epoch past 98, or add a compatible-change declaration under packages/runtime-host/protocol-compatible-changes/ naming this file. Also: this is the authoritative keyword table + traversal — the desktop projection re-encodes the same structure, so keep them from drifting (see the note on the desktop switch).
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -448,29 +449,89 @@ function capabilityOffer(
}
function toolInputSchema(tool: MakaTool): Record<string, unknown> {
- const schema = toJSONSchema(requireZodSchema(tool), {
- io: "input",
- target: "draft-07",
- unrepresentable: "any",
- cycles: "ref",
- reused: "inline",
- });
- delete schema.$schema;
- if (schema.type !== "object") {
- throw new Error(
- `Desktop native capability tool schema must be an object: ${tool.name}`,
- );
+ if (tool.parameters instanceof z.ZodType) {
+ const schema = toJSONSchema(tool.parameters, {
+ io: "input",
+ target: "draft-07",
+ unrepresentable: "any",
+ cycles: "ref",
+ reused: "inline",
+ });
+ delete schema.$schema;
+ if (schema.type !== "object") {
+ throw new Error(
+ `Desktop native capability tool schema must be an object: ${tool.name}`,
+ );
+ }
+ return Object.freeze(schema);
}
- return Object.freeze(schema);
+
+ const wrapper = tool.parameters as JsonSchemaWrapper | undefined;
+ if (wrapper && typeof wrapper.jsonSchema === "object" && wrapper.jsonSchema) {
+ const schema = wrapper.jsonSchema;
+ if (typeof schema === "object" && schema !== null) {
+ return Object.freeze(projectClientCapabilitySchema(schema));
+ }
+ }
+
+ throw new Error(
+ `Desktop native capability tool has an unsupported schema type: ${tool.name}`,
+ );
+}
+
+interface JsonSchemaWrapper {
+ readonly jsonSchema?: Record<string, unknown>;
+}
+
+function projectClientCapabilitySchema(schema: Record<string, unknown>): Record<string, unknown> {
+ const result: Record<string, unknown> = {};
+ for (const [key, value] of Object.entries(schema)) {
+ if (!CLIENT_CAPABILITY_SCHEMA_KEYWORDS.has(key)) continue;
+ result[key] = projectClientCapabilitySchemaKeyword(key, value);
+ }
+ return result;
+}
+
+function projectClientCapabilitySchemaKeyword(key: string, value: unknown): unknown {
Review Comment:
P2 (second authority / duplication): this switch re-encodes the keyword->shape traversal that client-capability.ts:validateToolInputSchema.visit() already owns (record: properties/patternProperties/$defs/definitions; array: allOf/anyOf/oneOf; single-or-array: items; single: additionalProperties/propertyNames). This PR needed edits in three coupled sites for one keyword. Drift risk: add a future nested keyword to the shared set + protocol visit but forget this switch -> `default: return value` passes the subtree verbatim (incl. disallowed keywords) -> the protocol boundary rejects it -> the WHOLE registration fails to publish with an opaque error. Fix: export a single projectToolInputSchema() from @maka/runtime-host driven by the same shape table; desktop just picks which schema to hand in. (Also emits protocol-invalid schemas today: items:[] / empty composites -> boundary rejects the whole batch.)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix: support MCP json schema tools [maka]
Posted by "liugddx (via GitHub)" <gi...@apache.org>.
liugddx commented on code in PR #4553:
URL: https://github.com/apache/maka/pull/4553#discussion_r3931447356
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -658,7 +661,7 @@ function declaredToolInputSchema(tool: MakaTool): Record<string, unknown> {
})
: cloneDeclaredJsonSchema(tool);
delete schema.$schema;
- return schema;
+ return Object.freeze(projectClientCapabilitySchema(schema));
Review Comment:
**The imported shared projector is not used here.** This still calls the Desktop-local `projectClientCapabilitySchema()`, even though `projectToolInputSchema()` is imported and the protocol package owns the same keyword-shape table. The two paths already diverge for nested `items: []`: the shared helper drops it, while this copy preserves it and the later protocol decode rejects the whole descriptor. Please call `projectToolInputSchema(schema)` here and delete the local projector so there is one projection authority.
##########
packages/runtime/src/mcp-tools.ts:
##########
@@ -141,7 +143,12 @@ export function buildMcpToolsWithIdentities(
categoryHint: options.categoryHint ?? 'network_send',
...(options.hostAdmission ? { hostAdmission: options.hostAdmission } : {}),
...(options.recoveryMode ? { recoveryMode: options.recoveryMode } : {}),
- parameters: jsonSchema(descriptor.inputSchema),
+ parameters: jsonSchema(inputSchema, {
Review Comment:
**This validator is attached to a schema whose dialect was already discarded.** `normalizeToolInputSchema()` removes `$schema`, so `validateJsonSchemaInput(inputSchema, value)` defaults to Draft 2020-12 even when the MCP server declared Draft-07 or Draft-2019-09. The published capability schema is also a lossy projection, so a constraint can be enforced locally while remaining invisible to the model. Preserve the source dialect for validation and define the projection contract, or let the MCP server remain the sole authority.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix: support MCP json schema tools [maka]
Posted by "liugddx (via GitHub)" <gi...@apache.org>.
liugddx commented on code in PR #4553:
URL: https://github.com/apache/maka/pull/4553#discussion_r3932529019
##########
packages/runtime/src/ai-sdk-backend.ts:
##########
@@ -618,18 +618,42 @@ async function validateCodeModeToolInput(tool: MakaTool, input: unknown): Promis
throw invalidCodeModeToolArguments(tool.name, validator.errors);
}
+export function validateJsonSchemaInput(
+ schema: unknown,
+ input: unknown,
+):
+ | { readonly success: true; readonly value: unknown }
+ | { readonly success: false; readonly error: Error } {
+ const validator = compileCodeModeJsonSchema(schema);
+ if (!validator || validator(input)) return { success: true, value: input };
+ return {
+ success: false,
+ error: new Error(schemaErrorSummary(validator.errors)),
+ };
+}
+
+function hasDraft7TupleItems(value: unknown): boolean {
Review Comment:
**This dialect probe scans instance data, not only schema nodes.** A Draft-2020 schema containing `const: {items: []}` or `examples: [{items: []}]` is forced into the Draft-07 validator even when `$schema` explicitly says Draft-2020. Draft-2020 constraints such as `unevaluatedProperties` can then be ignored. Restrict the walk to schema-bearing keywords and let an explicit dialect win.
##########
apps/desktop/src/main/runtime-host-native-capabilities.ts:
##########
@@ -658,7 +661,7 @@ function declaredToolInputSchema(tool: MakaTool): Record<string, unknown> {
})
: cloneDeclaredJsonSchema(tool);
delete schema.$schema;
- return schema;
+ return Object.freeze(projectClientCapabilitySchema(schema));
Review Comment:
**The imported shared projector is still unused here.** This calls the Desktop-local `projectClientCapabilitySchema()`, so the protocol package and Desktop have two projection authorities. They already disagree for `items: []`: the shared helper drops it, while this copy preserves it and the later protocol decode rejects the tool. Please call `projectToolInputSchema(schema)` here and delete the local projector.
##########
packages/runtime/src/mcp-tools.ts:
##########
@@ -141,7 +143,12 @@ export function buildMcpToolsWithIdentities(
categoryHint: options.categoryHint ?? 'network_send',
...(options.hostAdmission ? { hostAdmission: options.hostAdmission } : {}),
...(options.recoveryMode ? { recoveryMode: options.recoveryMode } : {}),
- parameters: jsonSchema(descriptor.inputSchema),
+ parameters: jsonSchema(inputSchema, {
Review Comment:
**This attaches synchronous validation for untrusted MCP regexes.** Ajv executes `pattern`/`patternProperties` on the Runtime main thread before the MCP call and cannot observe the abort signal during catastrophic backtracking. A remote descriptor using `^(a+)+$` can freeze the process with a long non-matching argument. Use linear-time or isolated validation, or leave this constraint to the MCP server.
##########
scripts/pre-push-check.ps1:
##########
@@ -0,0 +1,85 @@
+#!/usr/bin/env pwsh
+<##
+.SYNOPSIS
+ Pre-push checks for Windows. The commands mirror the CI checks that are
+ executable on Windows and include the PR's relevant dist test suites.
+#>
+
+$ErrorActionPreference = "Continue"
+$repoRoot = Split-Path -Parent $PSScriptRoot
+$failed = $false
+
+function Run-Check {
+ param(
+ [string]$Label,
+ [scriptblock]$Command
+ )
+
+ Write-Host (">>> " + $Label + " ...") -ForegroundColor Cyan
+ & $Command
Review Comment:
**`$LASTEXITCODE` alone does not reliably detect PowerShell command-not-found errors.** With `$ErrorActionPreference = "Continue"`, a missing `node`/`npm` command can emit a non-terminating error while leaving `$LASTEXITCODE` unchanged, so `Run-Check` can print `OK` and return a false-green result. Check `$?` as well, or make command failures terminating and catch them.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org