Skip to content

Commit 0904c0b

Browse files
committed
feat: Update version to 0.3.9, add ajv-errors support, and refactor JSON schema creation
1 parent ed7b7b3 commit 0904c0b

File tree

10 files changed

+112
-73
lines changed

10 files changed

+112
-73
lines changed

deno.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/deno.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@mcpc/core",
3-
"version": "0.3.8",
3+
"version": "0.3.9",
44
"repository": {
55
"type": "git",
66
"url": "git+https://github.com/mcpc-tech/mcpc.git"
@@ -29,6 +29,7 @@
2929
"@segment/ajv-human-errors": "npm:@segment/ajv-human-errors@^2.15.0",
3030
"@std/assert": "jsr:@std/assert@1",
3131
"ajv": "npm:ajv@^8.17.1",
32+
"ajv-errors": "npm:ajv-errors@^3.0.0",
3233
"ajv-formats": "npm:ajv-formats@^3.0.1",
3334
"cheerio": "npm:cheerio@^1.0.0",
3435
"jsonrepair": "npm:jsonrepair@^3.13.0"

packages/core/src/executors/agentic/agentic-executor.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,15 @@ export class AgenticExecutor {
104104

105105
const result: CallToolResult = { content: [] };
106106
this.appendToolSchemas(result, definitionsOf, hasDefinitions);
107+
108+
// If no schemas were added (all requested tools already in hasDefinitions), give feedback
109+
if (result.content.length === 0 && definitionsOf.length > 0) {
110+
result.content.push({
111+
type: "text",
112+
text: `All requested tool schemas are already in hasDefinitions. You can now call a tool using "${this.USE_TOOL_KEY}".`,
113+
});
114+
}
115+
107116
return result;
108117
}
109118

@@ -221,7 +230,8 @@ export class AgenticExecutor {
221230
}
222231
}
223232

224-
// Tool not found
233+
// Tool not found - this should not happen if schema validation is working
234+
// but keep as a fallback for safety
225235
if (executeSpan) {
226236
executeSpan.setAttributes({
227237
toolType: "not_found",
@@ -230,17 +240,15 @@ export class AgenticExecutor {
230240
endSpan(executeSpan);
231241
}
232242

233-
this.logger.warning({
234-
message: "Tool not found",
235-
useTool,
236-
availableTools: this.allToolNames,
237-
});
238-
239-
const result: CallToolResult = {
240-
content: [],
243+
return {
244+
content: [
245+
{
246+
type: "text",
247+
text: `Tool "${useTool}" not found. Available tools: ${this.allToolNames.join(", ")}.`,
248+
},
249+
],
250+
isError: true,
241251
};
242-
this.appendToolSchemas(result, definitionsOf, hasDefinitions);
243-
return result;
244252
} catch (error) {
245253
// Catch any unexpected errors
246254
if (executeSpan) {

packages/core/src/executors/agentic/agentic-sampling-registrar.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { jsonSchema } from "../../utils/schema.ts";
22
import type { SamplingConfig } from "../../types.ts";
3-
import { createGoogleCompatibleJSONSchema } from "../../utils/common/provider.ts";
3+
import { createModelCompatibleJSONSchema } from "../../utils/common/provider.ts";
44
import type { ComposableMCPServer } from "../../compose.ts";
55
import { CompiledPrompts } from "../../prompts/index.ts";
66
import { SamplingExecutor } from "../sampling/agentic-sampling-executor.ts";
@@ -62,7 +62,7 @@ export function registerAgenticSamplingTool(
6262
name,
6363
description,
6464
jsonSchema<Record<string, unknown>>(
65-
createGoogleCompatibleJSONSchema(schema as Record<string, unknown>),
65+
createModelCompatibleJSONSchema(schema as Record<string, unknown>),
6666
),
6767
async (args: Record<string, unknown>) => {
6868
return await samplingExecutor.executeSampling(

packages/core/src/executors/agentic/agentic-tool-registrar.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { jsonSchema, type Schema } from "../../utils/schema.ts";
22
import type { RegisterToolParams } from "../../types.ts";
3-
import { createGoogleCompatibleJSONSchema } from "../../utils/common/provider.ts";
3+
import { createModelCompatibleJSONSchema } from "../../utils/common/provider.ts";
44
import type { ComposableMCPServer } from "../../compose.ts";
55
import { CompiledPrompts } from "../../prompts/index.ts";
66
import { AgenticExecutor } from "./agentic-executor.ts";
@@ -51,7 +51,7 @@ export function registerAgenticTool(
5151
name,
5252
description,
5353
jsonSchema<Record<string, unknown>>(
54-
createGoogleCompatibleJSONSchema(schema as Record<string, unknown>),
54+
createModelCompatibleJSONSchema(schema as Record<string, unknown>),
5555
),
5656
async (args: Record<string, unknown>) => {
5757
return await agenticExecutor.execute(

packages/core/src/executors/workflow/workflow-sampling-registrar.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { jsonSchema } from "../../utils/schema.ts";
22
import type { SamplingConfig } from "../../types.ts";
33
import type { MCPCStep } from "../../utils/state.ts";
44
import { WorkflowState } from "../../utils/state.ts";
5-
import { createGoogleCompatibleJSONSchema } from "../../utils/common/provider.ts";
5+
import { createModelCompatibleJSONSchema } from "../../utils/common/provider.ts";
66
import type { ComposableMCPServer } from "../../compose.ts";
77
import { CompiledPrompts } from "../../prompts/index.ts";
88
import { createArgsDefFactory } from "../../factories/args-def-factory.ts";
@@ -75,7 +75,7 @@ export function registerWorkflowSamplingTool(
7575
name,
7676
baseDescription,
7777
jsonSchema<Record<string, unknown>>(
78-
createGoogleCompatibleJSONSchema(schema as Record<string, unknown>),
78+
createModelCompatibleJSONSchema(schema as Record<string, unknown>),
7979
),
8080
async (args: Record<string, unknown>) => {
8181
try {

packages/core/src/executors/workflow/workflow-tool-registrar.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { jsonSchema } from "../../utils/schema.ts";
22
import type { RegisterWorkflowToolParams } from "../../types.ts";
33
import { WorkflowState } from "../../utils/state.ts";
4-
import { createGoogleCompatibleJSONSchema } from "../../utils/common/provider.ts";
4+
import { createModelCompatibleJSONSchema } from "../../utils/common/provider.ts";
55
import { WorkflowExecutor } from "./workflow-executor.ts";
66
import type { ComposableMCPServer } from "../../compose.ts";
77
import { CompiledPrompts } from "../../prompts/index.ts";
@@ -65,7 +65,7 @@ export function registerAgenticWorkflowTool(
6565
name,
6666
toolDescription,
6767
jsonSchema<Record<string, unknown>>(
68-
createGoogleCompatibleJSONSchema(argsDef),
68+
createModelCompatibleJSONSchema(argsDef),
6969
),
7070
async (args: Record<string, unknown>) => {
7171
try {

packages/core/src/factories/args-def-factory.ts

Lines changed: 30 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -171,41 +171,6 @@ Workflow step definitions - provide ONLY on initial call.
171171
USE_TOOL_KEY: string = "useTool",
172172
): JSONSchema {
173173
const allOf = [
174-
// When hasDefinitions is empty, definitionsOf must be provided
175-
{
176-
if: {
177-
properties: {
178-
hasDefinitions: {
179-
type: "array",
180-
maxItems: 0,
181-
},
182-
},
183-
required: ["hasDefinitions"],
184-
},
185-
then: {
186-
required: ["definitionsOf"],
187-
},
188-
},
189-
// When useTool is present, hasDefinitions must contain that tool
190-
...toolNameToDetailList.map(
191-
([toolName, _toolDetail]: [string, unknown]) => {
192-
return {
193-
if: {
194-
properties: { [USE_TOOL_KEY]: { const: toolName } },
195-
required: [USE_TOOL_KEY],
196-
},
197-
then: {
198-
properties: {
199-
hasDefinitions: {
200-
type: "array",
201-
contains: { const: toolName },
202-
},
203-
},
204-
required: ["hasDefinitions"],
205-
},
206-
};
207-
},
208-
),
209174
// When a specific tool is selected, its parameters must be provided
210175
...toolNameToDetailList.map(
211176
([toolName, _toolDetail]: [string, unknown]) => {
@@ -216,6 +181,11 @@ Workflow step definitions - provide ONLY on initial call.
216181
},
217182
then: {
218183
required: [toolName],
184+
errorMessage: {
185+
required: {
186+
[toolName]: `Tool "${toolName}" is selected but its parameters are missing. Please provide "${toolName}": { ...parameters }.`,
187+
},
188+
},
219189
},
220190
};
221191
},
@@ -234,6 +204,9 @@ Workflow step definitions - provide ONLY on initial call.
234204
type: "string",
235205
enum: allToolNames,
236206
description: useToolDescription,
207+
errorMessage: {
208+
enum: `Invalid tool name. Available tools: ${allToolNames.join(", ")}.`,
209+
},
237210
},
238211
hasDefinitions: {
239212
type: "array",
@@ -264,6 +237,28 @@ Workflow step definitions - provide ONLY on initial call.
264237
schema.allOf = allOf;
265238
}
266239

240+
// Add conditional validation: if definitionsOf is empty/missing, useTool is required
241+
if (allToolNames.length > 0) {
242+
const thenClause = {
243+
required: [USE_TOOL_KEY],
244+
errorMessage: {
245+
required: {
246+
[USE_TOOL_KEY]: `No tool selected. Please specify "${USE_TOOL_KEY}" to select one of: ${allToolNames.join(", ")}. Or use "definitionsOf" with tool names to get their schemas first.`,
247+
},
248+
},
249+
};
250+
Object.assign(schema, {
251+
if: {
252+
// definitionsOf is not provided OR is empty array
253+
anyOf: [
254+
{ not: { required: ["definitionsOf"] } },
255+
{ properties: { definitionsOf: { type: "array", maxItems: 0 } } },
256+
],
257+
},
258+
then: thenClause,
259+
});
260+
}
261+
267262
return schema;
268263
},
269264

packages/core/src/utils/common/provider.ts

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,36 +36,48 @@ export const createGoogleJSONSchema = (
3636
};
3737

3838
/**
39-
* Creates a Google-compatible JSON schema by removing unsupported features
39+
* Creates a model-compatible JSON schema by removing validation-only features
4040
*
41-
* Google provider restrictions:
41+
* Always removes:
42+
* - errorMessage: AJV-specific custom error messages, not part of JSON Schema spec
43+
*
44+
* Google provider restrictions (when GEMINI_PREFERRED_FORMAT is enabled):
4245
* - Does not support additionalProperties in schema definitions (at any level)
4346
* - Does not support oneOf, allOf, or anyOf at the top level in input_schema
4447
* @see https://ai.google.dev/api/caching#Schema
4548
*/
46-
export const createGoogleCompatibleJSONSchema = (
49+
export const createModelCompatibleJSONSchema = (
4750
schema: Record<string, unknown>,
4851
): Record<string, unknown> => {
49-
if (!GEMINI_PREFERRED_FORMAT) {
50-
return schema;
51-
}
52+
// Keys to always remove (not part of JSON Schema spec, used by validators only)
53+
const validatorOnlyKeys = ["errorMessage"];
54+
55+
// Keys to remove only for Gemini
56+
const geminiRestrictedKeys = GEMINI_PREFERRED_FORMAT
57+
? ["additionalProperties"]
58+
: [];
5259

53-
// Remove top-level composition keywords
54-
const { oneOf: _oneOf, allOf: _allOf, anyOf: _anyOf, ...cleanSchema } =
55-
schema;
60+
const keysToRemove = new Set([...validatorOnlyKeys, ...geminiRestrictedKeys]);
61+
62+
// Remove top-level composition keywords for Gemini
63+
let cleanSchema = schema;
64+
if (GEMINI_PREFERRED_FORMAT) {
65+
const { oneOf: _oneOf, allOf: _allOf, anyOf: _anyOf, ...rest } = schema;
66+
cleanSchema = rest;
67+
}
5668

57-
// Recursively remove additionalProperties at all levels
58-
const removeAdditionalProperties = (obj: any): any => {
69+
// Recursively clean schema
70+
const cleanRecursively = (obj: unknown): unknown => {
5971
if (Array.isArray(obj)) {
60-
return obj.map(removeAdditionalProperties);
72+
return obj.map(cleanRecursively);
6173
}
6274

6375
if (obj && typeof obj === "object") {
6476
const result: Record<string, unknown> = {};
6577

6678
for (const [key, value] of Object.entries(obj)) {
67-
if (key !== "additionalProperties") {
68-
result[key] = removeAdditionalProperties(value);
79+
if (!keysToRemove.has(key)) {
80+
result[key] = cleanRecursively(value);
6981
}
7082
}
7183

@@ -75,5 +87,5 @@ export const createGoogleCompatibleJSONSchema = (
7587
return obj;
7688
};
7789

78-
return removeAdditionalProperties(cleanSchema);
90+
return cleanRecursively(cleanSchema) as Record<string, unknown>;
7991
};

packages/core/src/utils/schema-validator.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,40 @@
11
import { Ajv } from "ajv";
22
import addFormats from "ajv-formats";
3+
import ajvErrors from "ajv-errors";
34
import { AggregateAjvError } from "@segment/ajv-human-errors";
45

5-
// Singleton Ajv instance
6+
// Singleton Ajv instance with custom error message support
67
const ajv = new Ajv({
78
allErrors: true,
89
verbose: true,
910
});
1011

1112
addFormats.default(ajv);
13+
ajvErrors.default(ajv);
1214

1315
export function validateSchema(
1416
args: Record<string, unknown>,
1517
schema: Record<string, unknown>,
1618
): { valid: boolean; error?: string } {
1719
const validate = ajv.compile(schema);
1820
if (!validate(args)) {
19-
const errors = new AggregateAjvError(validate.errors!);
21+
const errors = validate.errors!;
22+
23+
// If there are custom errorMessage errors, use only those
24+
const customErrors = errors.filter((err) => err.keyword === "errorMessage");
25+
if (customErrors.length > 0) {
26+
const messages = [...new Set(customErrors.map((err) => err.message))];
27+
return {
28+
valid: false,
29+
error: messages.join("; "),
30+
};
31+
}
32+
33+
// Fallback to human-readable error formatting
34+
const aggregateError = new AggregateAjvError(errors);
2035
return {
2136
valid: false,
22-
error: errors.message,
37+
error: aggregateError.message,
2338
};
2439
}
2540
return { valid: true };

0 commit comments

Comments
 (0)