Skip to content

Commit 207da65

Browse files
authored
fix(search): read Search MCP date filters without an offset as UTC (#9798)
A date filter sent without its UTC offset failed validation, and the client resent the same value with Z appended, costing a full extra round. Accept the offset-less form and read it as UTC up front. tools/list is unchanged.
1 parent bc8b55f commit 207da65

2 files changed

Lines changed: 36 additions & 9 deletions

File tree

‎apps/sim/lib/api/contracts/knowledge/mcp.ts‎

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,30 @@ export const organizationKnowledgeMcpContract = defineRouteContract({
1919

2020
const LIVE_SEARCH_MCP_DEFAULT_TOP_K = { federated: 30, scoped: 20 } as const
2121

22+
const HAS_UTC_OFFSET = /(Z|[+-]\d{2}:\d{2})$/
23+
2224
/**
23-
* A datetime field without the regex its JSON Schema would otherwise carry beside
24-
* `format: "date-time"`. Every tools/list call resends the schema, and parsing still rejects a
25-
* string that is not an ISO datetime with an offset.
25+
* An ISO datetime filter that reads a value without an offset as UTC, which is what a client
26+
* resends after that value is rejected, so the call succeeds on the first try. Its JSON Schema
27+
* keeps `format: "date-time"` and the field's description but not the regex, which every
28+
* tools/list call would resend.
2629
*/
27-
function withoutPatternInJsonSchema<T extends z.ZodType>(schema: T): T {
28-
return schema.meta({ pattern: undefined })
30+
function mcpDateFilter(field: z.ZodOptional<z.ZodString>) {
31+
return z.iso
32+
.datetime({ offset: true, local: true })
33+
.transform((value) => (HAS_UTC_OFFSET.test(value) ? value : `${value}Z`))
34+
.optional()
35+
.meta({ pattern: undefined, description: field.description })
2936
}
3037

3138
const { startDate, endDate, modifiedAfter, modifiedBefore } = workspaceSearchFiltersSchema.shape
3239

3340
/** The date filters of the Search MCP tools, as their tools/list schema presents them. */
3441
const mcpDateFilterFields = {
35-
startDate: withoutPatternInJsonSchema(startDate),
36-
endDate: withoutPatternInJsonSchema(endDate),
37-
modifiedAfter: withoutPatternInJsonSchema(modifiedAfter),
38-
modifiedBefore: withoutPatternInJsonSchema(modifiedBefore),
42+
startDate: mcpDateFilter(startDate),
43+
endDate: mcpDateFilter(endDate),
44+
modifiedAfter: mcpDateFilter(modifiedAfter),
45+
modifiedBefore: mcpDateFilter(modifiedBefore),
3946
}
4047

4148
export const liveSearchMcpSchema = searchWorkspaceInputFieldsSchema

‎apps/sim/lib/knowledge/mcp/server.protocol.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,26 @@ describe('Search MCP protocol', () => {
9898
expect(read.content).toEqual([
9999
{ type: 'text', text: expect.stringContaining('Read evidence') },
100100
])
101+
const offsetless = await client.callTool({
102+
name: 'search',
103+
arguments: {
104+
query: 'release',
105+
startDate: '2026-09-28T00:00:00',
106+
endDate: '2026-10-08T00:00',
107+
modifiedAfter: '2026-09-01T08:00:00-07:00',
108+
},
109+
})
110+
expect(offsetless.isError).not.toBe(true)
111+
expect(mocks.liveSearch.mock.lastCall?.[0].input.filters).toMatchObject({
112+
startDate: '2026-09-28T00:00:00Z',
113+
endDate: '2026-10-08T00:00Z',
114+
modifiedAfter: '2026-09-01T08:00:00-07:00',
115+
})
116+
const notADate = await client.callTool({
117+
name: 'search',
118+
arguments: { query: 'release', startDate: '2026-09-28' },
119+
})
120+
expect(notADate.isError).toBe(true)
101121
} finally {
102122
await client.close()
103123
await server.close()

0 commit comments

Comments
 (0)