@@ -6,8 +6,8 @@ import { getErrorMessage } from '@sim/utils/errors'
66import { SIM_DOCS_URL } from '@sim/utils/site'
77
88/**
9- * Exercises the public ownership challenge, host isolation, the RFC 9727 API catalog, and the
10- * app-wide JSON 404 through a running local app.
9+ * Exercises the public ownership challenge, host isolation, the RFC 9727 API catalog, the
10+ * app-wide JSON 404, and the Markdown 404 for `Accept: text/markdown` through a running local app.
1111 */
1212const logger = createLogger ( 'McpHostE2E' )
1313const CHALLENGE_PATH = '/.well-known/openai-apps-challenge'
@@ -17,6 +17,8 @@ const PROTECTED_RESOURCE_PATH = '/.well-known/oauth-protected-resource'
1717const API_CATALOG_PATH = '/.well-known/api-catalog'
1818const API_CATALOG_CONTENT_TYPE =
1919 'application/linkset+json; profile="https://www.rfc-editor.org/info/rfc9727"'
20+ const MARKDOWN_ACCEPT = 'text/markdown'
21+ const BROWSER_ACCEPT = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
2022const startedAt = new Date ( ) . toISOString ( )
2123
2224const configuredBaseUrl = process . env . MCP_HOST_E2E_BASE_URL
@@ -43,18 +45,25 @@ const requests: {
4345 path : string
4446 userAgent : string
4547 host : string
48+ accept : string
4649 status : number
4750} [ ] = [ ]
4851
49- async function request ( path : string , userAgent : string , host = MCP_HOST , method = 'GET' ) {
52+ async function request (
53+ path : string ,
54+ userAgent : string ,
55+ host = MCP_HOST ,
56+ method = 'GET' ,
57+ accept = '*/*'
58+ ) {
5059 // boundary-raw-fetch: exercise the real proxy and route over local HTTP.
5160 const response = await fetch ( new URL ( path , baseUrl ) , {
5261 method,
53- headers : { Host : host , 'User-Agent' : userAgent } ,
62+ headers : { Host : host , 'User-Agent' : userAgent , Accept : accept } ,
5463 redirect : 'error' ,
5564 signal : AbortSignal . timeout ( 120_000 ) ,
5665 } )
57- requests . push ( { method, path, userAgent, host, status : response . status } )
66+ requests . push ( { method, path, userAgent, host, accept , status : response . status } )
5867 const body = await response . text ( )
5968 return { response, body }
6069}
@@ -122,7 +131,11 @@ try {
122131 response . headers . get ( 'link' ) ,
123132 `<${ baseUrl . origin } ${ API_CATALOG_PATH } >; rel="api-catalog"`
124133 )
125- const [ api ] = JSON . parse ( body ) . linkset
134+ const [ catalog , api ] = JSON . parse ( body ) . linkset
135+ assert . deepEqual ( catalog , {
136+ anchor : `${ baseUrl . origin } ${ API_CATALOG_PATH } ` ,
137+ item : [ { href : `${ baseUrl . origin } /api/v2` } ] ,
138+ } )
126139 assert . equal ( api . anchor , `${ baseUrl . origin } /api/v2` )
127140 assert . equal ( api [ 'service-desc' ] [ 0 ] . href , `${ SIM_DOCS_URL } /openapi.json` )
128141 assert . equal ( api . status [ 0 ] . href , `${ baseUrl . origin } /api/health` )
@@ -138,6 +151,77 @@ try {
138151 assert . match ( response . headers . get ( 'link' ) ?? '' , / r e l = " a p i - c a t a l o g " $ / )
139152 } )
140153
154+ for ( const path of [ '/not-a-real-page' , '/not-a-real/nested/page' ] ) {
155+ await check ( `unknown page ${ path } answers a Markdown client in Markdown` , async ( ) => {
156+ const { response, body } = await request (
157+ path ,
158+ 'python-requests/2.32.3' ,
159+ baseUrl . host ,
160+ 'GET' ,
161+ MARKDOWN_ACCEPT
162+ )
163+ assert . equal ( response . status , 404 )
164+ assert . equal ( response . headers . get ( 'content-type' ) , 'text/markdown; charset=utf-8' )
165+ assert . match ( response . headers . get ( 'vary' ) ?? '' , / \b A c c e p t \b / )
166+ assert . match ( response . headers . get ( 'cache-control' ) ?? '' , / \b n o - s t o r e \b / )
167+ assert . match ( body , / ^ # P a g e n o t f o u n d \n / )
168+ assert . match ( body , / \/ l l m s \. t x t \) / )
169+ } )
170+ }
171+
172+ await check ( 'an unknown page answers a Markdown POST in Markdown, not 405' , async ( ) => {
173+ const { response } = await request (
174+ '/not-a-real-page' ,
175+ 'python-requests/2.32.3' ,
176+ baseUrl . host ,
177+ 'POST' ,
178+ MARKDOWN_ACCEPT
179+ )
180+ assert . equal ( response . status , 404 )
181+ assert . equal ( response . headers . get ( 'content-type' ) , 'text/markdown; charset=utf-8' )
182+ } )
183+
184+ await check ( 'a client that refuses Markdown with q=0 gets the HTML 404' , async ( ) => {
185+ const { response } = await request (
186+ '/not-a-real-page' ,
187+ 'python-requests/2.32.3' ,
188+ baseUrl . host ,
189+ 'GET' ,
190+ 'text/html, text/markdown;q=0'
191+ )
192+ assert . equal ( response . status , 404 )
193+ assert . match ( response . headers . get ( 'content-type' ) ?? '' , / ^ t e x t \/ h t m l / )
194+ } )
195+
196+ await check ( 'unknown page still answers a browser with the HTML 404' , async ( ) => {
197+ const { response } = await request (
198+ '/not-a-real-page' ,
199+ 'Mozilla/5.0' ,
200+ baseUrl . host ,
201+ 'GET' ,
202+ BROWSER_ACCEPT
203+ )
204+ assert . equal ( response . status , 404 )
205+ assert . match ( response . headers . get ( 'content-type' ) ?? '' , / ^ t e x t \/ h t m l / )
206+ } )
207+
208+ await check ( 'a real page is not shadowed by the Markdown 404' , async ( ) => {
209+ const { response } = await request ( '/' , 'Mozilla/5.0' , baseUrl . host , 'GET' , MARKDOWN_ACCEPT )
210+ assert . equal ( response . status , 200 )
211+ } )
212+
213+ await check ( 'an unknown API path keeps its JSON 404 for a Markdown client' , async ( ) => {
214+ const { response, body } = await request (
215+ '/api/not-a-real-route' ,
216+ 'Mozilla/5.0' ,
217+ baseUrl . host ,
218+ 'GET' ,
219+ MARKDOWN_ACCEPT
220+ )
221+ assert . equal ( response . status , 404 )
222+ assert . deepEqual ( JSON . parse ( body ) , { error : 'Not found' } )
223+ } )
224+
141225 await check ( 'application host still serves health checks' , async ( ) => {
142226 const { response } = await request ( '/api/health' , 'Mozilla/5.0' , baseUrl . host )
143227 assert . equal ( response . status , 200 )
0 commit comments