Skip to content

Commit 9625957

Browse files
authored
fix(mcp): stop keyless setup from triggering OAuth (#364)
1 parent 001b399 commit 9625957

3 files changed

Lines changed: 18 additions & 26 deletions

File tree

src/index.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ type ServerProfile = {
8787
primary?: boolean;
8888
/** Accept tokens minted for the legacy /v2/mcp resource during migration. */
8989
acceptLegacyAudience?: boolean;
90+
/** Publish OAuth discovery metadata for clients configuring this surface. */
91+
advertiseOAuth: boolean;
9092
};
9193

9294
/** Registers a tool onto an instance; a subset of the FastMCP surface. */
@@ -238,7 +240,11 @@ function createOAuthChallengeResponse(
238240
const errorMessage =
239241
error instanceof Error ? error.message : String(error || 'Unauthorized');
240242
const wwwAuthenticate = [
241-
`resource_metadata="${escapeWWWAuthenticateValue(getOAuthProtectedResourceMetadataUrl(profile))}"`,
243+
...(profile.advertiseOAuth
244+
? [
245+
`resource_metadata="${escapeWWWAuthenticateValue(getOAuthProtectedResourceMetadataUrl(profile))}"`,
246+
]
247+
: []),
242248
'error="invalid_token"',
243249
`error_description="${escapeWWWAuthenticateValue(errorMessage)}"`,
244250
].join(', ');
@@ -856,6 +862,7 @@ function makeFullProfile(): ServerProfile {
856862
acceptApiKeys: true,
857863
acceptLegacyAudience:
858864
account && process.env.MCP_OAUTH_ACCEPT_LEGACY_V2_MCP_AUD !== 'false',
865+
advertiseOAuth: account,
859866
primary: true,
860867
};
861868
}
@@ -887,6 +894,7 @@ function makeSearchProfile({ primary = false }: { primary?: boolean } = {}): Ser
887894
// deployment explicitly enables the same profile flag used by primary.
888895
acceptApiKeys: !oauthOnly,
889896
requireManagedOAuth: oauthOnly,
897+
advertiseOAuth: true,
890898
primary,
891899
};
892900
}
@@ -905,7 +913,7 @@ function createServer(profile: ServerProfile): FastMCP<SessionData> {
905913
logger: new ConsoleLogger(),
906914
roots: { enabled: false },
907915
oauth: {
908-
enabled: isMcpOAuthEnabled(),
916+
enabled: isMcpOAuthEnabled() && profile.advertiseOAuth,
909917
protectedResource: {
910918
authorizationServers: [getOAuthIssuer()],
911919
bearerMethodsSupported: ['header'],

tests/mcp-search-profile.test.mjs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@ test('search surface accepts a token minted for its own resource', async (t) =>
618618
});
619619

620620
test('full surface still exposes its complete tool set alongside the search surface', async (t) => {
621-
const { fullPort, issuerUrl } = await startHostedServer(t);
621+
const { fullPort } = await startHostedServer(t);
622622

623623
// Full surface is reachable on its own port with all tools intact.
624624
const names = await listTools(fullPort, '/v2/mcp', { 'x-api-key': 'fc-test' });
@@ -628,18 +628,12 @@ test('full surface still exposes its complete tool set alongside the search surf
628628
assert.ok(names.includes('firecrawl_parse'));
629629
assert.ok(names.length > SEARCH_TOOLS.length);
630630

631-
// Its protected-resource metadata stays origin-level and unchanged.
631+
// The anonymous full surface accepts credentials but does not advertise
632+
// OAuth, so clients do not start login while configuring keyless MCP.
632633
const prm = await fetch(
633634
`http://127.0.0.1:${fullPort}/.well-known/oauth-protected-resource`
634635
);
635-
assert.equal(prm.status, 200);
636-
assert.deepEqual(await prm.json(), {
637-
authorization_servers: [issuerUrl],
638-
bearer_methods_supported: ['header'],
639-
resource: 'https://mcp.firecrawl.dev/v2/mcp',
640-
resource_name: 'Firecrawl MCP',
641-
scopes_supported: ['firecrawl:global'],
642-
});
636+
assert.equal(prm.status, 404);
643637
});
644638

645639
test('primary search profile is OAuth-only, six-tool frozen, and ready without keyless configuration', async (t) => {

tests/mcp-smoke.test.mjs

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ async function httpToolCall(port, { endpoint = '/v2/mcp', id, headers, params })
328328
});
329329
}
330330

331-
test('HTTP cloud transport preserves Firecrawl OAuth and well-known routes', async (t) => {
331+
test('HTTP cloud keyless transport preserves app challenge without advertising OAuth', async (t) => {
332332
const backend = await startFakeFirecrawlBackend();
333333
t.after(() => backend.close());
334334
const port = await getFreePort();
@@ -360,14 +360,7 @@ test('HTTP cloud transport preserves Firecrawl OAuth and well-known routes', asy
360360
const prm = await fetch(
361361
`http://127.0.0.1:${port}/.well-known/oauth-protected-resource`
362362
);
363-
assert.equal(prm.status, 200);
364-
assert.deepEqual(await prm.json(), {
365-
authorization_servers: [backend.url],
366-
bearer_methods_supported: ['header'],
367-
resource: 'https://mcp.firecrawl.dev/v2/mcp',
368-
resource_name: 'Firecrawl MCP',
369-
scopes_supported: ['firecrawl:global'],
370-
});
363+
assert.equal(prm.status, 404);
371364

372365
const unauthenticated = await fetch(`http://127.0.0.1:${port}/v2/mcp`, {
373366
body: JSON.stringify({
@@ -895,7 +888,7 @@ test('HTTP cloud transport swaps an fco_ OAuth token for its introspected API ke
895888
assert.equal(stderr.includes('TypeError'), false, stderr);
896889
});
897890

898-
test('HTTP cloud transport rejects an inactive fco_ token with an OAuth challenge', async (t) => {
891+
test('HTTP cloud keyless transport rejects inactive OAuth without advertising login', async (t) => {
899892
const backend = await startFakeFirecrawlBackend();
900893
t.after(() => backend.close());
901894

@@ -926,10 +919,7 @@ test('HTTP cloud transport rejects an inactive fco_ token with an OAuth challeng
926919
assert.equal(toolCall.status, 401);
927920
const wwwAuthenticate = toolCall.headers.get('www-authenticate') ?? '';
928921
assert.match(wwwAuthenticate, /^Bearer /);
929-
assert.match(
930-
wwwAuthenticate,
931-
/resource_metadata="https:\/\/mcp\.firecrawl\.dev\/\.well-known\/oauth-protected-resource"/
932-
);
922+
assert.equal(wwwAuthenticate.includes('resource_metadata='), false);
933923
assert.match(wwwAuthenticate, /error="invalid_token"/);
934924
const body = await toolCall.json();
935925
assert.equal(body.error, 'invalid_token');

0 commit comments

Comments
 (0)