Skip to content

Commit 089748b

Browse files
authored
Merge pull request #378 from lokicik/fix/search-limit-schema
fix: constrain search limit schema
2 parents 8c93c56 + 3ae017f commit 089748b

3 files changed

Lines changed: 50 additions & 1 deletion

File tree

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -888,7 +888,7 @@ const searchToolBaseFields = {
888888
.describe(
889889
'Return query-relevant highlights for each search result. Set to false to keep the original search snippets.'
890890
),
891-
limit: z.number().optional(),
891+
limit: z.number().int().min(1).max(100).optional(),
892892
tbs: z.string().optional(),
893893
filter: z.string().optional(),
894894
location: z.string().optional(),

tests/mcp-search-profile.test.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,9 @@ test('primary search profile uses the strict marketplace search tool, not the fu
831831
assert.ok(search, 'primary profile must register firecrawl_search');
832832
assert.doesNotMatch(JSON.stringify(search.inputSchema), /scrapeOptions/);
833833
assert.doesNotMatch(search.description ?? '', /search_feedback|refund/i);
834+
assert.equal(search.inputSchema.properties.limit.type, 'integer');
835+
assert.equal(search.inputSchema.properties.limit.minimum, 1);
836+
assert.equal(search.inputSchema.properties.limit.maximum, 100);
834837

835838
const response = await jsonRpc(port, SEARCH_ENDPOINT, {
836839
id: 13,

tests/mcp-smoke.test.mjs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,9 @@ test('HTTP cloud keyless transport preserves app challenge without advertising O
631631
);
632632
assert.equal(searchTool.inputSchema.properties.highlights.type, 'boolean');
633633
assert.equal('default' in searchTool.inputSchema.properties.highlights, false);
634+
assert.equal(searchTool.inputSchema.properties.limit.type, 'integer');
635+
assert.equal(searchTool.inputSchema.properties.limit.minimum, 1);
636+
assert.equal(searchTool.inputSchema.properties.limit.maximum, 100);
634637

635638
assert.equal(
636639
backend.requests.filter((request) => request.url === '/api/oauth/introspect').length,
@@ -717,6 +720,49 @@ test('HTTP cloud transport calls Firecrawl API with authenticated session', asyn
717720
assert.equal(stderr.includes('TypeError'), false, stderr);
718721
});
719722

723+
test('HTTP cloud transport rejects invalid search limits before calling Firecrawl', async (t) => {
724+
const fakeApi = await startFakeFirecrawlApi();
725+
t.after(() => fakeApi.close());
726+
727+
const port = await getFreePort();
728+
const child = spawnServer({
729+
CLOUD_SERVICE: 'true',
730+
FASTMCP_ENDPOINT: '/v2/mcp',
731+
FIRECRAWL_API_URL: fakeApi.url,
732+
FIRECRAWL_OAUTH_ISSUER: fakeApi.url,
733+
FIRECRAWL_OAUTH_INTROSPECT_SECRET: 'test-secret',
734+
HTTP_STREAMABLE_SERVER: 'true',
735+
PORT: String(port),
736+
});
737+
t.after(() => stopChild(child));
738+
await waitForHealth(port, child);
739+
740+
const searchRequestsBefore = fakeApi.requests.filter(
741+
(request) => request.url === '/v2/search'
742+
).length;
743+
for (const [index, limit] of [0, 1.5, 101].entries()) {
744+
const response = await httpToolCall(port, {
745+
id: 400 + index,
746+
headers: { 'x-api-key': 'fc-test' },
747+
params: {
748+
arguments: { limit, query: 'invalid search limit' },
749+
name: 'firecrawl_search',
750+
},
751+
});
752+
assert.equal(response.status, 200);
753+
const message = parseSseJson(await response.text());
754+
assert.equal(
755+
Boolean(message.error) || message.result?.isError === true,
756+
true,
757+
JSON.stringify(message)
758+
);
759+
}
760+
const searchRequestsAfter = fakeApi.requests.filter(
761+
(request) => request.url === '/v2/search'
762+
).length;
763+
assert.equal(searchRequestsAfter, searchRequestsBefore);
764+
});
765+
720766
class StdioMcpClient {
721767
#buffer = '';
722768
#child;

0 commit comments

Comments
 (0)