Skip to content

Commit 89dce25

Browse files
committed
Fix MCP discovery timestamp CAS precision (#5754)
Match configuration generations within the JavaScript millisecond window so PostgreSQL microseconds do not suppress valid publication.
1 parent ce6d0f6 commit 89dce25

2 files changed

Lines changed: 36 additions & 5 deletions

File tree

apps/sim/lib/mcp/service.test.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ vi.mock('@/lib/mcp/storage', () => ({
180180
getMcpCacheType: () => 'memory',
181181
}))
182182

183-
import { mcpService } from '@/lib/mcp/service'
183+
import { getTimestampMillisecondBounds, mcpService } from '@/lib/mcp/service'
184184
import { McpOauthAuthorizationRequiredError } from '@/lib/mcp/types'
185185

186186
const mockLogger = vi.mocked(loggerMock.createLogger).mock.results.at(-1)?.value
@@ -218,6 +218,23 @@ function tool(name: string, serverId: string) {
218218
}
219219
}
220220

221+
describe('getTimestampMillisecondBounds', () => {
222+
it('includes PostgreSQL sub-millisecond precision but excludes the next millisecond', () => {
223+
const { startInclusive, endExclusive } = getTimestampMillisecondBounds(
224+
'2026-01-01T00:00:00.123Z'
225+
)
226+
const startMicroseconds = startInclusive.getTime() * 1_000
227+
const endMicroseconds = endExclusive.getTime() * 1_000
228+
const isWithinBounds = (candidateMicroseconds: number) =>
229+
candidateMicroseconds >= startMicroseconds && candidateMicroseconds < endMicroseconds
230+
231+
// PostgreSQL can retain any of these extra microseconds even though the
232+
// JavaScript Date used as the generation token is truncated to .123.
233+
expect(isWithinBounds(startMicroseconds + 999)).toBe(true)
234+
expect(isWithinBounds(endMicroseconds)).toBe(false)
235+
})
236+
})
237+
221238
describe('McpService.discoverTools per-server caching', () => {
222239
beforeEach(async () => {
223240
vi.clearAllMocks()

apps/sim/lib/mcp/service.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { createLogger } from '@sim/logger'
77
import { describeError, getErrorMessage } from '@sim/utils/errors'
88
import { sleep } from '@sim/utils/helpers'
99
import { backoffWithJitter } from '@sim/utils/retry'
10-
import { and, eq, isNull, lte, or } from 'drizzle-orm'
10+
import { and, eq, gte, isNull, lt, lte, or } from 'drizzle-orm'
1111
import { isTest } from '@/lib/core/config/env-flags'
1212
import { generateRequestId } from '@/lib/core/utils/request'
1313
import { McpClient } from '@/lib/mcp/client'
@@ -53,6 +53,17 @@ function failureCacheKey(workspaceId: string, serverId: string): string {
5353
return `workspace:${workspaceId}:server:${serverId}:failure`
5454
}
5555

56+
export function getTimestampMillisecondBounds(timestamp: string): {
57+
startInclusive: Date
58+
endExclusive: Date
59+
} {
60+
const startInclusive = new Date(timestamp)
61+
return {
62+
startInclusive,
63+
endExclusive: new Date(startInclusive.getTime() + 1),
64+
}
65+
}
66+
5667
const FAILURE_CACHE_SENTINEL: McpTool[] = []
5768

5869
type DiscoveryOutcome =
@@ -417,12 +428,13 @@ class McpService {
417428
): Promise<boolean> {
418429
try {
419430
const now = new Date()
420-
const configUpdatedAt = new Date(update.configUpdatedAt)
431+
const configUpdatedAt = getTimestampMillisecondBounds(update.configUpdatedAt)
421432
const publicationConditions = and(
422433
eq(mcpServers.id, serverId),
423434
eq(mcpServers.workspaceId, workspaceId),
424435
isNull(mcpServers.deletedAt),
425-
eq(mcpServers.updatedAt, configUpdatedAt),
436+
gte(mcpServers.updatedAt, configUpdatedAt.startInclusive),
437+
lt(mcpServers.updatedAt, configUpdatedAt.endExclusive),
426438
or(
427439
isNull(mcpServers.lastToolsRefresh),
428440
lte(mcpServers.lastToolsRefresh, update.discoveryStartedAt)
@@ -531,6 +543,7 @@ class McpService {
531543
discoveryStartedAt: Date
532544
): Promise<boolean> {
533545
try {
546+
const configUpdatedAtBounds = getTimestampMillisecondBounds(configUpdatedAt)
534547
const updatedServers = await db
535548
.update(mcpServers)
536549
.set({
@@ -544,7 +557,8 @@ class McpService {
544557
eq(mcpServers.id, serverId),
545558
eq(mcpServers.workspaceId, workspaceId),
546559
isNull(mcpServers.deletedAt),
547-
eq(mcpServers.updatedAt, new Date(configUpdatedAt)),
560+
gte(mcpServers.updatedAt, configUpdatedAtBounds.startInclusive),
561+
lt(mcpServers.updatedAt, configUpdatedAtBounds.endExclusive),
548562
or(
549563
isNull(mcpServers.lastToolsRefresh),
550564
lte(mcpServers.lastToolsRefresh, discoveryStartedAt)

0 commit comments

Comments
 (0)