Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions apps/sim/blocks/blocks/serply.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Search } from '@sim/emcn/icons'
import type { BlockConfig, BlockMeta } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import type { SearchResponse } from '@/tools/serply/search'

export const SerplyBlock: BlockConfig<SearchResponse> = {
type: 'serply',
name: 'Serply',
description: 'Search the web using Serply',
authMode: AuthMode.ApiKey,
longDescription: 'Integrate Serply into the workflow. Can search the web.',
docsLink: 'https://docs.sim.ai/integrations/serply',
category: 'tools',
integrationType: IntegrationType.Search,
bgColor: '#4F46E5',
icon: Search,
canvasPresentation: {
defaultTitle: 'Serply',
sentences: {
default: [
{ text: 'Search Google for', field: 'query', core: true },
{ text: ', returning up to', field: 'num', after: 'results' },
],
},
},
subBlocks: [
{
id: 'query',
title: 'Search Query',
type: 'short-input',
placeholder: 'Enter your search query...',
required: true,
},
{
id: 'num',
title: 'Number of Results',
type: 'dropdown',
options: [
{ label: '10', id: '10' },
{ label: '20', id: '20' },
{ label: '30', id: '30' },
{ label: '50', id: '50' },
],
},
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your Serply API key',
password: true,
required: true,
},
],
tools: {
access: ['serply_search'],
},
inputs: {
query: { type: 'string', description: 'Search query terms' },
apiKey: { type: 'string', description: 'Serply API key' },
num: { type: 'number', description: 'Number of results' },
},
outputs: {
searchResults: { type: 'json', description: 'Search results data' },
},
}

export const SerplyBlockMeta = {
tags: ['web-scraping', 'seo'],
url: 'https://serply.io',
} as const satisfies BlockMeta
3 changes: 3 additions & 0 deletions apps/sim/blocks/registry-maps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ import { SendblueBlock, SendblueBlockMeta } from '@/blocks/blocks/sendblue'
import { SendGridBlock, SendGridBlockMeta } from '@/blocks/blocks/sendgrid'
import { SentryBlock, SentryBlockMeta } from '@/blocks/blocks/sentry'
import { SerperBlock, SerperBlockMeta } from '@/blocks/blocks/serper'
import { SerplyBlock, SerplyBlockMeta } from '@/blocks/blocks/serply'
import { ServiceNowBlock, ServiceNowBlockMeta } from '@/blocks/blocks/servicenow'
import { SESBlock, SESBlockMeta } from '@/blocks/blocks/ses'
import { SftpBlock, SftpBlockMeta } from '@/blocks/blocks/sftp'
Expand Down Expand Up @@ -614,6 +615,7 @@ export const BLOCK_REGISTRY: Record<string, BlockConfig> = {
sendgrid: SendGridBlock,
sentry: SentryBlock,
serper: SerperBlock,
serply: SerplyBlock,
servicenow: ServiceNowBlock,
ses: SESBlock,
sftp: SftpBlock,
Expand Down Expand Up @@ -914,6 +916,7 @@ export const BLOCK_META_REGISTRY: Record<string, BlockMeta> = {
sendgrid: SendGridBlockMeta,
sentry: SentryBlockMeta,
serper: SerperBlockMeta,
serply: SerplyBlockMeta,
servicenow: ServiceNowBlockMeta,
ses: SESBlockMeta,
sftp: SftpBlockMeta,
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/tools/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3929,6 +3929,7 @@ import {
updateProjectTool,
} from '@/tools/sentry'
import { serperSearchTool } from '@/tools/serper'
import { searchTool as serplySearchTool } from '@/tools/serply/search'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated tool metadata left stale

Medium Severity

serply_search is registered in the live tool registry, but apps/sim/tools/generated/tool-ids.ts, tool-metadata.ts, and tool-outputs.ts were not regenerated. Client and serializer code resolve tools through those artifacts via hasToolId / getToolParams / getToolMetadata, so the new tool is treated as unknown and tool-metadata:check will fail.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a4129fe. Configure here.

import {
servicenowAddIncidentCommentTool,
servicenowAggregateTool,
Expand Down Expand Up @@ -6441,6 +6442,7 @@ export const tools: Record<string, ToolConfig> = {
github_latest_commit: githubLatestCommitTool,
github_latest_commit_v2: githubLatestCommitV2Tool,
serper_search: serperSearchTool,
serply_search: serplySearchTool,
similarweb_website_overview: similarwebWebsiteOverviewTool,
similarweb_traffic_visits: similarwebTrafficVisitsTool,
similarweb_bounce_rate: similarwebBounceRateTool,
Expand Down
56 changes: 56 additions & 0 deletions apps/sim/tools/serply/search.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { searchTool } from '@/tools/serply/search'

describe('serply searchTool', () => {
it('sends the api key, accept header, and an explicit user agent', () => {
const headers = searchTool.request.headers({ query: 'sim', apiKey: 'test-key' })

expect(headers['X-Api-Key']).toBe('test-key')
expect(headers.Accept).toBe('application/json')
expect(headers['User-Agent']).toBeTruthy()
})

it('builds the query URL with the optional num param', () => {
const url = (searchTool.request.url as (params: any) => string)({
query: 'sim workflows',
apiKey: 'test-key',
num: 20,
})

expect(url).toBe('https://api.serply.io/v1/search/?q=sim+workflows&num=20')
})

it('maps organic results into searchResults', async () => {
const response = new Response(
JSON.stringify({
results: [
{ title: 'Sim', link: 'https://sim.ai', description: 'AI workspace' },
{ title: 'No link', description: 'dropped upstream? kept here' },
],
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
)

const result = await searchTool.transformResponse!(response, {} as never)

expect(result.success).toBe(true)
expect(result.output.searchResults).toEqual([
{ title: 'Sim', link: 'https://sim.ai', snippet: 'AI workspace' },
{ title: 'No link', link: '', snippet: 'dropped upstream? kept here' },
])
})

it('returns an empty array when the response has no results', async () => {
const response = new Response(JSON.stringify({}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})

const result = await searchTool.transformResponse!(response, {} as never)

expect(result.output.searchResults).toEqual([])
})
})
95 changes: 95 additions & 0 deletions apps/sim/tools/serply/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type { OutputProperty, ToolConfig, ToolResponse } from '@/tools/types'

export const SERPLY_SEARCH_RESULT_OUTPUT_PROPERTIES = {
title: { type: 'string', description: 'Result title' },
link: { type: 'string', description: 'Result URL' },
snippet: { type: 'string', description: 'Result description/snippet', optional: true },
} as const satisfies Record<string, OutputProperty>

export interface SearchParams {
query: string
apiKey: string
num?: number
}

export interface SearchResult {
title: string
link: string
snippet?: string
}

export interface SearchResponse extends ToolResponse {
output: {
searchResults: SearchResult[]
}
}

export const searchTool: ToolConfig<SearchParams, SearchResponse> = {
id: 'serply_search',
name: 'Web Search',
description:
'A web search tool that provides access to Google search results through the Serply SERP API. Returns organic results with titles, links, and snippets.',
version: '1.0.0',

params: {
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The search query (e.g., "latest AI news", "best restaurants in NYC")',
},
num: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to return (e.g., 10, 20, 50)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Serply API Key',
},
},

request: {
url: (params) => {
const url = new URL('https://api.serply.io/v1/search/')
url.searchParams.set('q', params.query)
if (params.num) url.searchParams.set('num', String(Number(params.num)))
return url.toString()
},
method: 'GET',
headers: (params) => ({
'X-Api-Key': params.apiKey,
Accept: 'application/json',
// Serply sits behind Cloudflare, which rejects requests without an
// explicit User-Agent, so always send one.
'User-Agent': 'sim-serply-tool',
}),
},

transformResponse: async (response: Response) => {
const data = await response.json()
const results = Array.isArray(data.results) ? data.results : []

const searchResults: SearchResult[] = results.map((item: any) => ({
title: item.title || '',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Avoid untyped response items

The explicit any removes compiler checks from every Serply result mapping, allowing invalid response assumptions to pass type checking as the integration evolves. Model the response item or narrow it from unknown; the same explicit any pattern also appears in the URL-builder test.

Context Used: TypeScript conventions and type safety (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

link: item.link || '',
snippet: item.description || undefined,
}))

return {
success: true,
output: { searchResults },
}
},

outputs: {
searchResults: {
type: 'array',
description: 'Organic search results with titles, links, and snippets',
items: { type: 'object', properties: SERPLY_SEARCH_RESULT_OUTPUT_PROPERTIES },
},
},
}