Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 226 additions & 0 deletions scripts/tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,119 @@
"default": 100,
"description": "The number of items to return"
},
"filters": {
"type": "object",
"description": "Filter expression with AND/OR logic. Use 'conditions' for filter criteria.",
"properties": {
"operator": {
"type": "string",
"enum": [
"and",
"or"
],
"description": "Logical operator to combine conditions (default: and)"
},
"conditions": {
"type": "array",
"description": "Array of filter conditions to apply",
"items": {
"type": "object",
"properties": {
"property_key": {
"type": "string",
"description": "The property key to filter on (e.g., 'done', 'created_date', 'links')"
},
"condition": {
"type": "string",
"enum": [
"eq",
"ne",
"gt",
"gte",
"lt",
"lte",
"empty",
"nempty",
"in",
"nin",
"contains",
"ncontains",
"all"
],
"description": "Filter condition: eq/ne (equals), gt/gte/lt/lte (comparison), empty/nempty (null check), in/nin/all (arrays), contains/ncontains (text)"
},
"text": {
"type": "string",
"description": "Text value for text property filters"
},
"number": {
"type": "number",
"description": "Number value for number property filters"
},
"checkbox": {
"type": "boolean",
"description": "Boolean value for checkbox property filters"
},
"date": {
"type": "string",
"description": "ISO 8601 date for date property filters (e.g., '2026-01-29T00:00:00Z')"
},
"select": {
"type": "string",
"description": "Tag ID for single-select property filters"
},
"multi_select": {
"type": "array",
"items": {
"type": "string"
},
"description": "Array of tag IDs for multi-select property filters"
},
"objects": {
"type": "array",
"items": {
"type": "string"
},
"description": "Array of object IDs for relation property filters"
},
"files": {
"type": "array",
"items": {
"type": "string"
},
"description": "Array of file IDs for file property filters"
},
"url": {
"type": "string",
"description": "URL value for URL property filters"
},
"email": {
"type": "string",
"description": "Email value for email property filters"
},
"phone": {
"type": "string",
"description": "Phone value for phone property filters"
}
},
"required": [
"property_key",
"condition"
]
}
},
"filters": {
"type": "array",
"description": "Nested filter expressions for complex AND/OR logic (optional)",
"items": {
"type": "object",
"additionalProperties": true,
"description": "Nested FilterExpression (same structure as parent)"
}
}
},
"additionalProperties": false
},
"query": {
"type": "string",
"description": "The text to search within object names and content; use types field for type filtering"
Expand Down Expand Up @@ -4419,6 +4532,119 @@
"default": 100,
"description": "The number of items to return"
},
"filters": {
"type": "object",
"description": "Filter expression with AND/OR logic. Use 'conditions' for filter criteria.",
"properties": {
"operator": {
"type": "string",
"enum": [
"and",
"or"
],
"description": "Logical operator to combine conditions (default: and)"
},
"conditions": {
"type": "array",
"description": "Array of filter conditions to apply",
"items": {
"type": "object",
"properties": {
"property_key": {
"type": "string",
"description": "The property key to filter on (e.g., 'done', 'created_date', 'links')"
},
"condition": {
"type": "string",
"enum": [
"eq",
"ne",
"gt",
"gte",
"lt",
"lte",
"empty",
"nempty",
"in",
"nin",
"contains",
"ncontains",
"all"
],
"description": "Filter condition: eq/ne (equals), gt/gte/lt/lte (comparison), empty/nempty (null check), in/nin/all (arrays), contains/ncontains (text)"
},
"text": {
"type": "string",
"description": "Text value for text property filters"
},
"number": {
"type": "number",
"description": "Number value for number property filters"
},
"checkbox": {
"type": "boolean",
"description": "Boolean value for checkbox property filters"
},
"date": {
"type": "string",
"description": "ISO 8601 date for date property filters (e.g., '2026-01-29T00:00:00Z')"
},
"select": {
"type": "string",
"description": "Tag ID for single-select property filters"
},
"multi_select": {
"type": "array",
"items": {
"type": "string"
},
"description": "Array of tag IDs for multi-select property filters"
},
"objects": {
"type": "array",
"items": {
"type": "string"
},
"description": "Array of object IDs for relation property filters"
},
"files": {
"type": "array",
"items": {
"type": "string"
},
"description": "Array of file IDs for file property filters"
},
"url": {
"type": "string",
"description": "URL value for URL property filters"
},
"email": {
"type": "string",
"description": "Email value for email property filters"
},
"phone": {
"type": "string",
"description": "Phone value for phone property filters"
}
},
"required": [
"property_key",
"condition"
]
}
},
"filters": {
"type": "array",
"description": "Nested filter expressions for complex AND/OR logic (optional)",
"items": {
"type": "object",
"additionalProperties": true,
"description": "Nested FilterExpression (same structure as parent)"
}
}
},
"additionalProperties": false
},
"query": {
"type": "string",
"description": "The text to search within object names and content; use types field for type filtering"
Expand Down
120 changes: 120 additions & 0 deletions src/openapi/__tests__/parser-filters.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { readFileSync } from "fs";
import { JSONSchema7 as IJsonSchema } from "json-schema";
import { OpenAPIV3 } from "openapi-types";
import { fileURLToPath } from "url";
import { dirname, resolve } from "path";
import { beforeAll, describe, expect, it } from "vitest";
import { OpenAPIToMCPConverter } from "../parser";

// Regression tests for the flattened `FilterExpression` schema (PR #32).
//
// The real Anytype spec models `filters` as a recursive `FilterExpression`
// whose `conditions` are a 12-variant `oneOf` (`FilterItem`). We flatten that
// into a single agent-friendly object. These tests pin that flattening against
// the checked-in spec so it can't silently regress back to `{}` or drop fields.

const SPEC_PATH = resolve(dirname(fileURLToPath(import.meta.url)), "../../../scripts/openapi.json");

// The full set of value fields, one per `FilterItem` variant in the spec.
const VALUE_FIELDS: Record<string, "string" | "number" | "boolean" | "array"> = {
text: "string",
number: "number",
checkbox: "boolean",
date: "string",
select: "string",
multi_select: "array",
objects: "array",
files: "array",
url: "string",
email: "string",
phone: "string",
};

// Short-form condition enum the API actually accepts (see FilterCondition
// x-enum-varnames). All 13 must be exposed.
const EXPECTED_CONDITIONS = [
"eq",
"ne",
"gt",
"gte",
"lt",
"lte",
"empty",
"nempty",
"in",
"nin",
"contains",
"ncontains",
"all",
];

describe("FilterExpression flattening", () => {
let filtersBySearchTool: Record<string, IJsonSchema>;

beforeAll(() => {
const spec = JSON.parse(readFileSync(SPEC_PATH, "utf-8")) as OpenAPIV3.Document;
const converter = new OpenAPIToMCPConverter(spec);
const { tools } = converter.convertToMCPTools();

const methods = Object.values(tools).flatMap((tool) => tool.methods);
filtersBySearchTool = {};
for (const name of ["search-space", "search-global"]) {
const method = methods.find((m) => m.name === name);
expect(method, `tool ${name} should exist`).toBeDefined();
filtersBySearchTool[name] = (method!.inputSchema as IJsonSchema).properties!.filters as IJsonSchema;
}
});

it("registers both search tools", () => {
expect(Object.keys(filtersBySearchTool).sort()).toEqual(["search-global", "search-space"]);
});

for (const toolName of ["search-space", "search-global"]) {
describe(toolName, () => {
it("exposes a flattened filters object (not an empty {} or a $ref)", () => {
const filters = filtersBySearchTool[toolName];
expect(filters).toBeDefined();
expect(filters.$ref).toBeUndefined();
expect(filters.type).toBe("object");
expect(filters.properties).toBeDefined();
expect(Object.keys(filters.properties!)).toEqual(expect.arrayContaining(["operator", "conditions", "filters"]));
});

it("exposes the and/or operator enum", () => {
const operator = filtersBySearchTool[toolName].properties!.operator as IJsonSchema;
expect(operator.type).toBe("string");
expect(operator.enum).toEqual(["and", "or"]);
});

it("exposes the full condition enum", () => {
const item = (filtersBySearchTool[toolName].properties!.conditions as IJsonSchema).items as IJsonSchema;
const condition = item.properties!.condition as IJsonSchema;
expect(condition.enum).toEqual(EXPECTED_CONDITIONS);
});

it("requires property_key and condition on each item", () => {
const item = (filtersBySearchTool[toolName].properties!.conditions as IJsonSchema).items as IJsonSchema;
expect(item.required).toEqual(["property_key", "condition"]);
expect((item.properties!.property_key as IJsonSchema).type).toBe("string");
});

it("exposes every type-specific value field", () => {
const item = (filtersBySearchTool[toolName].properties!.conditions as IJsonSchema).items as IJsonSchema;
for (const [field, type] of Object.entries(VALUE_FIELDS)) {
const prop = item.properties![field] as IJsonSchema;
expect(prop, `value field ${field} should exist`).toBeDefined();
expect(prop.type, `value field ${field} type`).toBe(type);
if (type === "array") {
expect((prop.items as IJsonSchema).type, `value field ${field} items`).toBe("string");
}
}
});

it("supports nested filter expressions", () => {
const nested = filtersBySearchTool[toolName].properties!.filters as IJsonSchema;
expect(nested.type).toBe("array");
expect((nested.items as IJsonSchema).type).toBe("object");
});
});
}
});
Loading