Skip to content

Commit c48255e

Browse files
committed
fix(mcp): add directory verification compatibility
1 parent ee5646c commit c48255e

6 files changed

Lines changed: 42 additions & 10 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
2+
3+
/** Public ownership challenge issued for the OpenAI plugin submission. */
4+
export const GET = withRouteHandler(
5+
async () =>
6+
new Response('lFJ1-XIWpHGRNcgzTPl2Y_yYCTWLvlIbAvNRD08EvLI', {
7+
headers: { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store' },
8+
})
9+
)

‎apps/sim/lib/api/mcp/host-routing.test.ts‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ describe('Sim MCP host routing', () => {
3030

3131
it.each([
3232
['/mcp', '/api/mcp'],
33+
['/.well-known/openai-apps-challenge', '/.well-known/openai-apps-challenge'],
3334
[
3435
'/.well-known/oauth-protected-resource/mcp',
3536
'/.well-known/oauth-protected-resource/api/mcp',
@@ -40,12 +41,17 @@ describe('Sim MCP host routing', () => {
4041
expect(resolveSimMcpHostPath('MCP.SIM.AI', pathname)).toBe(target)
4142
})
4243

43-
it.each(['/', '/login', '/workspace/ws-1', '/api/mcp', '/api/v2/workspaces', '/mcp/'])(
44-
'exposes nothing else: %s',
45-
(pathname) => {
46-
expect(resolveSimMcpHostPath('mcp.sim.ai', pathname)).toBe('not_found')
47-
}
48-
)
44+
it.each([
45+
'/',
46+
'/login',
47+
'/workspace/ws-1',
48+
'/api/mcp',
49+
'/api/v2/workspaces',
50+
'/mcp/',
51+
'/.well-known/openai-apps-challenge/other',
52+
])('exposes nothing else: %s', (pathname) => {
53+
expect(resolveSimMcpHostPath('mcp.sim.ai', pathname)).toBe('not_found')
54+
})
4955

5056
it.each(['mcp.sim.ai:443', 'mcp.sim.ai.', 'MCP.SIM.AI.:443'])(
5157
'recognizes the host spelled %s',

‎apps/sim/lib/api/mcp/host-routing.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { getBaseUrl } from '@/lib/core/utils/urls'
33

44
const PROTECTED_RESOURCE_METADATA = '/.well-known/oauth-protected-resource'
55
const AUTHORIZATION_SERVER_METADATA = '/.well-known/oauth-authorization-server'
6+
const OPENAI_APPS_CHALLENGE = '/.well-known/openai-apps-challenge'
67

78
/**
89
* A `Host` header as a URL authority under `protocol`: lower-cased, without the
@@ -45,7 +46,9 @@ export function resolveSimMcpHostPath(
4546
if (pathname === mcp.pathname) return SIM_MCP_ROUTE_PATH
4647
if (pathname === `${PROTECTED_RESOURCE_METADATA}${mcp.pathname}`) return internalMetadataPath
4748
if (!dedicated) return null
48-
return pathname === AUTHORIZATION_SERVER_METADATA ? pathname : 'not_found'
49+
return pathname === AUTHORIZATION_SERVER_METADATA || pathname === OPENAI_APPS_CHALLENGE
50+
? pathname
51+
: 'not_found'
4952
}
5053

5154
/**

‎apps/sim/lib/api/mcp/server.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export function createSimMcpServer(context: Omit<McpDispatchContext, 'signal'>):
116116
annotations: {
117117
title: 'Search operations',
118118
readOnlyHint: true,
119+
destructiveHint: false,
119120
idempotentHint: true,
120121
openWorldHint: false,
121122
},
@@ -133,6 +134,7 @@ export function createSimMcpServer(context: Omit<McpDispatchContext, 'signal'>):
133134
annotations: {
134135
title: 'Describe operation',
135136
readOnlyHint: true,
137+
destructiveHint: false,
136138
idempotentHint: true,
137139
openWorldHint: false,
138140
},
@@ -155,6 +157,7 @@ export function createSimMcpServer(context: Omit<McpDispatchContext, 'signal'>):
155157
annotations: {
156158
title: 'Read from Sim',
157159
readOnlyHint: true,
160+
destructiveHint: false,
158161
idempotentHint: true,
159162
openWorldHint: false,
160163
},

‎apps/sim/proxy.test.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,16 @@ describe('proxy on the dedicated MCP host', () => {
148148
it('serves nothing else on the MCP host', () => {
149149
expect(proxy(mcpRequest('/login', 'GET')).status).toBe(404)
150150
})
151+
152+
it.each(['', 'python-requests/2.32.3'])(
153+
'allows ownership verification with an automated user agent: %s',
154+
(userAgent) => {
155+
const request = mcpRequest('/.well-known/openai-apps-challenge', 'GET')
156+
if (userAgent) request.headers.set('user-agent', userAgent)
157+
expect(proxy(request).status).toBe(200)
158+
expect(proxy(mcpRequest('/.well-known/openai-apps-challenge/other', 'GET')).status).toBe(404)
159+
}
160+
)
151161
})
152162

153163
describe('proxy matcher', () => {

‎apps/sim/proxy.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -301,14 +301,15 @@ function handleSecurityFiltering(request: NextRequest): NextResponse | null {
301301
pathname.startsWith('/api/webhooks/tiktok') ||
302302
pathname.startsWith('/api/webhooks/agentmail')
303303
const isMcpEndpoint = pathname.startsWith('/api/mcp/')
304-
const isMcpOauthDiscoveryEndpoint =
304+
const isMcpDiscoveryEndpoint =
305305
pathname.startsWith('/.well-known/oauth-authorization-server') ||
306-
pathname.startsWith('/.well-known/oauth-protected-resource')
306+
pathname.startsWith('/.well-known/oauth-protected-resource') ||
307+
pathname === '/.well-known/openai-apps-challenge'
307308
const isSuspicious = SUSPICIOUS_UA_PATTERNS.some((pattern) => pattern.test(userAgent))
308309

309310
// Block suspicious requests, but exempt machine-to-machine endpoints that may
310311
// legitimately omit User-Agent headers (webhooks and MCP protocol discovery/calls).
311-
if (isSuspicious && !isWebhookEndpoint && !isMcpEndpoint && !isMcpOauthDiscoveryEndpoint) {
312+
if (isSuspicious && !isWebhookEndpoint && !isMcpEndpoint && !isMcpDiscoveryEndpoint) {
312313
logger.warn('Blocked suspicious request', {
313314
userAgent,
314315
ip: getClientIp(request),

0 commit comments

Comments
 (0)