From 79d9bea2d080739b355018bddcf8b70b962557ba Mon Sep 17 00:00:00 2001 From: Omar Rami Aldaajneh <134194853+utopianguide@users.noreply.github.com> Date: Sun, 3 May 2026 13:35:53 +0300 Subject: [PATCH 1/2] fix(mcp): resolve platformId so private (CUSTOM) pieces are discoverable (#12769) Co-authored-by: Hazem Adel --- .../api/src/app/mcp/tools/ap-list-pieces.ts | 9 +- .../api/src/app/mcp/tools/ap-setup-guide.ts | 4 +- .../server/api/src/app/mcp/tools/mcp-utils.ts | 7 +- .../test/integration/ce/mcp/mcp-tools.test.ts | 112 ++++++++++++++++++ .../cloud/mcp/mcp-piece-visibility.test.ts | 106 +++++++++++++++++ 5 files changed, 234 insertions(+), 4 deletions(-) create mode 100644 packages/server/api/test/integration/cloud/mcp/mcp-piece-visibility.test.ts diff --git a/packages/server/api/src/app/mcp/tools/ap-list-pieces.ts b/packages/server/api/src/app/mcp/tools/ap-list-pieces.ts index d8b4d414d72..17b0304e041 100644 --- a/packages/server/api/src/app/mcp/tools/ap-list-pieces.ts +++ b/packages/server/api/src/app/mcp/tools/ap-list-pieces.ts @@ -8,6 +8,7 @@ import { import { FastifyBaseLogger } from 'fastify' import { z } from 'zod' import { pieceMetadataService } from '../../pieces/metadata/piece-metadata-service' +import { projectService } from '../../project/project-service' import { mcpUtils } from './mcp-utils' const listPiecesSchema = z.object({ @@ -37,9 +38,13 @@ export const apListPiecesTool = (mcp: McpServer, log: FastifyBaseLogger): McpToo execute: async (args) => { try { const params = listPiecesSchema.parse(args ?? {}) + // Resolve platformId so private (CUSTOM) pieces owned by this project's + // platform show up alongside public (OFFICIAL) ones. + const project = await projectService(log).getOneOrThrow(mcp.projectId) const pieces = await pieceMetadataService(log).list({ projectId: mcp.projectId, - includeHidden: true, + platformId: project.platformId, + includeHidden: false, categories: params.categories as PieceCategory[] | undefined, tags: params.tags, searchQuery: params.searchQuery, @@ -78,7 +83,7 @@ export const apListPiecesTool = (mcp: McpServer, log: FastifyBaseLogger): McpToo name: piece.name, version: piece.version, projectId: mcp.projectId, - platformId: undefined, + platformId: project.platformId, }) if (fullPiece) { if (params.includeActions) { diff --git a/packages/server/api/src/app/mcp/tools/ap-setup-guide.ts b/packages/server/api/src/app/mcp/tools/ap-setup-guide.ts index f605a31e194..c40ad08c45b 100644 --- a/packages/server/api/src/app/mcp/tools/ap-setup-guide.ts +++ b/packages/server/api/src/app/mcp/tools/ap-setup-guide.ts @@ -58,11 +58,13 @@ async function connectionGuide(mcp: McpServer, log: FastifyBaseLogger, pieceName } } + // Resolve platformId so private (CUSTOM) pieces on this platform are discoverable. + const project = await projectService(log).getOneOrThrow(mcp.projectId) const piece = await pieceMetadataService(log).get({ name: pieceName, version: undefined, projectId: mcp.projectId, - platformId: undefined, + platformId: project.platformId, }) if (isNil(piece)) { diff --git a/packages/server/api/src/app/mcp/tools/mcp-utils.ts b/packages/server/api/src/app/mcp/tools/mcp-utils.ts index 2865cf062ed..569749869ec 100644 --- a/packages/server/api/src/app/mcp/tools/mcp-utils.ts +++ b/packages/server/api/src/app/mcp/tools/mcp-utils.ts @@ -4,6 +4,7 @@ import type { RouterAction, Step } from '@activepieces/shared' import { FastifyBaseLogger } from 'fastify' import { z } from 'zod' import { pieceMetadataService } from '../../pieces/metadata/piece-metadata-service' +import { projectService } from '../../project/project-service' const NON_INPUT_PROP_TYPES = new Set([ PropertyType.OAUTH2, @@ -142,7 +143,11 @@ async function lookupPieceComponent({ pieceName, componentName, componentType, p if (isNil(normalized)) { return { error: mcpToolError('Validation failed', new Error('pieceName is required')) } } - const piece = await pieceMetadataService(log).get({ name: normalized, projectId }) + // Resolve platformId so private (CUSTOM) pieces on this platform are findable by + // every tool that uses this helper (ap_add_step, ap_update_step, ap_get_piece_props, + // ap_validate_step_config, ap_run_action, etc.). + const project = await projectService(log).getOneOrThrow(projectId) + const piece = await pieceMetadataService(log).get({ name: normalized, projectId, platformId: project.platformId }) if (isNil(piece)) { return { error: { content: [{ type: 'text', text: `❌ Piece "${normalized}" not found. Use ap_list_pieces to get valid piece names.` }] } } } diff --git a/packages/server/api/test/integration/ce/mcp/mcp-tools.test.ts b/packages/server/api/test/integration/ce/mcp/mcp-tools.test.ts index f1fe2659999..98305074659 100644 --- a/packages/server/api/test/integration/ce/mcp/mcp-tools.test.ts +++ b/packages/server/api/test/integration/ce/mcp/mcp-tools.test.ts @@ -2413,4 +2413,116 @@ describe('MCP Tools integration', () => { expect(text(result)).not.toContain('special characters') }) + + // ── Private (CUSTOM) pieces visibility ─────────────────────────── + // MCP tools must resolve the caller's platformId so pieces created + // on that platform (pieceType: CUSTOM) are discoverable. Without the + // platformId filter, `pieceCache.filterPieceBasedOnType` drops them. + + it('90. ap_list_pieces — includes CUSTOM pieces belonging to the caller\'s platform', async () => { + const ctx = await createTestContext(app) + const mcp = makeMcp(ctx.project.id) + + const privatePiece = createMockPieceMetadata({ + name: '@activepieces/piece-private-custom', + displayName: 'Private Custom Piece', + version: '0.1.0', + pieceType: PieceType.CUSTOM, + packageType: PackageType.REGISTRY, + platformId: ctx.platform.id, + actions: { + run: { + name: 'run', + displayName: 'Run', + description: 'Runs the private action', + requireAuth: false, + props: {}, + }, + }, + triggers: {}, + }) + await db.save('piece_metadata', privatePiece) + + const result = await apListPiecesTool(mcp, mockLog).execute({ + searchQuery: 'private-custom', + }) + + expect(text(result)).toContain('✅') + expect(text(result)).toContain('@activepieces/piece-private-custom') + }) + + it('91. ap_get_piece_props — resolves CUSTOM pieces on the caller\'s platform via lookupPieceComponent', async () => { + const ctx = await createTestContext(app) + const mcp = makeMcp(ctx.project.id) + + const privatePiece = createMockPieceMetadata({ + name: '@activepieces/piece-private-lookup', + displayName: 'Private Lookup Piece', + version: '0.1.0', + pieceType: PieceType.CUSTOM, + packageType: PackageType.REGISTRY, + platformId: ctx.platform.id, + actions: { + do_thing: { + name: 'do_thing', + displayName: 'Do Thing', + description: 'Does a thing on the private piece', + requireAuth: false, + props: { + note: { type: 'SHORT_TEXT', displayName: 'Note', required: false }, + }, + }, + }, + triggers: {}, + }) + await db.save('piece_metadata', privatePiece) + + const result = await apGetPiecePropsTool(mcp, mockLog).execute({ + pieceName: '@activepieces/piece-private-lookup', + actionOrTriggerName: 'do_thing', + type: 'action', + }) + + // If the platformId resolution is broken, lookupPieceComponent returns + // "Piece not found" here. With the fix, the schema is returned. + expect(text(result)).not.toContain('not found') + expect(text(result)).toContain('do_thing') + }) + + it('92. ap_list_pieces — does NOT include CUSTOM pieces from a different platform', async () => { + // Two independent projects on two different platforms. Each platform's + // private piece must stay invisible to the other. + const ctxA = await createTestContext(app) + const ctxB = await createTestContext(app) + const mcpA = makeMcp(ctxA.project.id) + + const privateOfB = createMockPieceMetadata({ + name: '@activepieces/piece-private-only-b', + displayName: 'Private Piece Only for Platform B', + version: '0.1.0', + pieceType: PieceType.CUSTOM, + packageType: PackageType.REGISTRY, + platformId: ctxB.platform.id, + actions: { + run: { + name: 'run', + displayName: 'Run', + description: 'Only visible on platform B', + requireAuth: false, + props: {}, + }, + }, + triggers: {}, + }) + await db.save('piece_metadata', privateOfB) + + const result = await apListPiecesTool(mcpA, mockLog).execute({ + searchQuery: 'private-only-b', + }) + + // Require the call to have succeeded — otherwise an error response + // (which also wouldn't contain the piece name) would falsely pass. + expect(text(result)).toContain('✅') + expect(text(result)).not.toContain('@activepieces/piece-private-only-b') + }) }) diff --git a/packages/server/api/test/integration/cloud/mcp/mcp-piece-visibility.test.ts b/packages/server/api/test/integration/cloud/mcp/mcp-piece-visibility.test.ts new file mode 100644 index 00000000000..edde30c2cca --- /dev/null +++ b/packages/server/api/test/integration/cloud/mcp/mcp-piece-visibility.test.ts @@ -0,0 +1,106 @@ +import { beforeAll, afterAll, describe, it, expect } from 'vitest' +import { FastifyBaseLogger, FastifyInstance } from 'fastify' +import { + apId, + FilteredPieceBehavior, + McpServer, + McpServerStatus, + PackageType, + PieceType, +} from '@activepieces/shared' +import { setupTestEnvironment, teardownTestEnvironment } from '../../../helpers/test-setup' +import { createTestContext } from '../../../helpers/test-context' +import { db } from '../../../helpers/db' +import { createMockPieceMetadata } from '../../../helpers/mocks' +import { pieceCache } from '../../../../src/app/pieces/metadata/piece-cache' +import { apListPiecesTool } from '../../../../src/app/mcp/tools/ap-list-pieces' + +let app: FastifyInstance +let mockLog: FastifyBaseLogger + +beforeAll(async () => { + app = await setupTestEnvironment() + mockLog = app.log +}) + +afterAll(async () => { + await teardownTestEnvironment() +}) + +describe('MCP piece visibility', () => { + it('ap_list_pieces — does NOT return pieces hidden by platform admin (BLOCKED behavior)', async () => { + const blockedPieceName = '@activepieces/piece-hidden-by-admin' + + const ctx = await createTestContext(app, { + platform: { + filteredPieceBehavior: FilteredPieceBehavior.BLOCKED, + filteredPieceNames: [blockedPieceName], + }, + }) + const mcp = makeMcp(ctx.project.id) + + const blockedPiece = createMockPieceMetadata({ + name: blockedPieceName, + displayName: 'Hidden By Admin', + version: '0.1.0', + pieceType: PieceType.OFFICIAL, + packageType: PackageType.REGISTRY, + platformId: undefined, + actions: {}, + triggers: {}, + }) + await db.save('piece_metadata', blockedPiece) + await pieceCache(mockLog).setup() + + const result = await apListPiecesTool(mcp, mockLog).execute({}) + + expect(text(result)).toContain('✅') + expect(text(result)).not.toContain(blockedPieceName) + }) + + it('ap_list_pieces — returns pieces NOT in the platform blocklist', async () => { + const visiblePieceName = '@activepieces/piece-visible' + + const ctx = await createTestContext(app, { + platform: { + filteredPieceBehavior: FilteredPieceBehavior.BLOCKED, + filteredPieceNames: ['@activepieces/piece-something-else'], + }, + }) + const mcp = makeMcp(ctx.project.id) + + const visiblePiece = createMockPieceMetadata({ + name: visiblePieceName, + displayName: 'Visible Piece', + version: '0.1.0', + pieceType: PieceType.OFFICIAL, + packageType: PackageType.REGISTRY, + platformId: undefined, + actions: {}, + triggers: {}, + }) + await db.save('piece_metadata', visiblePiece) + await pieceCache(mockLog).setup() + + const result = await apListPiecesTool(mcp, mockLog).execute({}) + + expect(text(result)).toContain('✅') + expect(text(result)).toContain(visiblePieceName) + }) +}) + +function makeMcp(projectId: string): McpServer { + return { + id: apId(), + created: new Date().toISOString(), + updated: new Date().toISOString(), + projectId, + status: McpServerStatus.ENABLED, + token: apId(), + enabledTools: null, + } +} + +function text(result: { content: Array<{ type: 'text', text: string }> }): string { + return result.content.map(c => c.text).join('\n') +} From dee7830fe35731b77e8e238fd66f1aaa7e039235 Mon Sep 17 00:00:00 2001 From: Harmatta <136149988+Harmatta@users.noreply.github.com> Date: Sun, 3 May 2026 12:54:51 +0200 Subject: [PATCH 2/2] feat(pieces): add Proxycurl piece (#12149) Co-authored-by: Ahmad Tash --- bun.lock | 12 +++ .../pieces/community/proxycurl/.eslintrc.json | 33 +++++++ packages/pieces/community/proxycurl/README.md | 5 + .../pieces/community/proxycurl/package.json | 17 ++++ .../pieces/community/proxycurl/src/index.ts | 41 ++++++++ .../src/lib/actions/custom-api-call.ts | 15 +++ .../src/lib/actions/get-company-profile.ts | 28 ++++++ .../src/lib/actions/get-person-profile.ts | 28 ++++++ .../src/lib/actions/lookup-person-email.ts | 34 +++++++ .../src/lib/actions/search-people.ts | 48 ++++++++++ .../proxycurl/src/lib/common/client.ts | 93 +++++++++++++++++++ .../pieces/community/proxycurl/tsconfig.json | 19 ++++ .../community/proxycurl/tsconfig.lib.json | 15 +++ tsconfig.base.json | 3 + 14 files changed, 391 insertions(+) create mode 100644 packages/pieces/community/proxycurl/.eslintrc.json create mode 100644 packages/pieces/community/proxycurl/README.md create mode 100644 packages/pieces/community/proxycurl/package.json create mode 100644 packages/pieces/community/proxycurl/src/index.ts create mode 100644 packages/pieces/community/proxycurl/src/lib/actions/custom-api-call.ts create mode 100644 packages/pieces/community/proxycurl/src/lib/actions/get-company-profile.ts create mode 100644 packages/pieces/community/proxycurl/src/lib/actions/get-person-profile.ts create mode 100644 packages/pieces/community/proxycurl/src/lib/actions/lookup-person-email.ts create mode 100644 packages/pieces/community/proxycurl/src/lib/actions/search-people.ts create mode 100644 packages/pieces/community/proxycurl/src/lib/common/client.ts create mode 100644 packages/pieces/community/proxycurl/tsconfig.json create mode 100644 packages/pieces/community/proxycurl/tsconfig.lib.json diff --git a/bun.lock b/bun.lock index 72682c024cd..535c1aeeed4 100644 --- a/bun.lock +++ b/bun.lock @@ -5552,6 +5552,16 @@ "tslib": "^2.3.0", }, }, + "packages/pieces/community/proxycurl": { + "name": "@activepieces/piece-proxycurl", + "version": "0.1.0", + "dependencies": { + "@activepieces/pieces-common": "workspace:*", + "@activepieces/pieces-framework": "workspace:*", + "@activepieces/shared": "workspace:*", + "tslib": "2.6.2", + }, + }, "packages/pieces/community/pushbullet": { "name": "@activepieces/piece-pushbullet", "version": "0.1.4", @@ -9366,6 +9376,8 @@ "@activepieces/piece-promptmate": ["@activepieces/piece-promptmate@workspace:packages/pieces/community/promptmate"], + "@activepieces/piece-proxycurl": ["@activepieces/piece-proxycurl@workspace:packages/pieces/community/proxycurl"], + "@activepieces/piece-pushbullet": ["@activepieces/piece-pushbullet@workspace:packages/pieces/community/pushbullet"], "@activepieces/piece-pushover": ["@activepieces/piece-pushover@workspace:packages/pieces/community/pushover"], diff --git a/packages/pieces/community/proxycurl/.eslintrc.json b/packages/pieces/community/proxycurl/.eslintrc.json new file mode 100644 index 00000000000..610e15b05bf --- /dev/null +++ b/packages/pieces/community/proxycurl/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "extends": [ + "../../../../.eslintrc.base.json" + ], + "ignorePatterns": [ + "!**/*" + ], + "overrides": [ + { + "files": [ + "*.ts", + "*.tsx", + "*.js", + "*.jsx" + ], + "rules": {} + }, + { + "files": [ + "*.ts", + "*.tsx" + ], + "rules": {} + }, + { + "files": [ + "*.js", + "*.jsx" + ], + "rules": {} + } + ] +} diff --git a/packages/pieces/community/proxycurl/README.md b/packages/pieces/community/proxycurl/README.md new file mode 100644 index 00000000000..dd57f909bc9 --- /dev/null +++ b/packages/pieces/community/proxycurl/README.md @@ -0,0 +1,5 @@ +# pieces-proxycurl + +## Building + +Run `turbo run build --filter=@activepieces/piece-proxycurl` to build the library. diff --git a/packages/pieces/community/proxycurl/package.json b/packages/pieces/community/proxycurl/package.json new file mode 100644 index 00000000000..c37f3237f7a --- /dev/null +++ b/packages/pieces/community/proxycurl/package.json @@ -0,0 +1,17 @@ +{ + "name": "@activepieces/piece-proxycurl", + "version": "0.1.0", + "type": "commonjs", + "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "scripts": { + "build": "tsc -p tsconfig.lib.json && cp package.json dist/", + "lint": "eslint 'src/**/*.ts'" + }, + "dependencies": { + "@activepieces/pieces-common": "workspace:*", + "@activepieces/pieces-framework": "workspace:*", + "@activepieces/shared": "workspace:*", + "tslib": "2.6.2" + } +} diff --git a/packages/pieces/community/proxycurl/src/index.ts b/packages/pieces/community/proxycurl/src/index.ts new file mode 100644 index 00000000000..d195757c046 --- /dev/null +++ b/packages/pieces/community/proxycurl/src/index.ts @@ -0,0 +1,41 @@ +import { createPiece, PieceAuth } from '@activepieces/pieces-framework'; +import { PieceCategory } from '@activepieces/shared'; +import { getPersonProfileAction } from './lib/actions/get-person-profile'; +import { getCompanyProfileAction } from './lib/actions/get-company-profile'; +import { searchPeopleAction } from './lib/actions/search-people'; +import { lookupPersonEmailAction } from './lib/actions/lookup-person-email'; +import { customApiCallAction } from './lib/actions/custom-api-call'; + +const markdownDescription = ` +To use Proxycurl: +1. Sign in to your Proxycurl account at https://nubela.co/proxycurl. +2. Open your dashboard and copy your API key. +3. Paste the API key here. + +Authentication note: +- Proxycurl's public SDK/docs show Bearer token authorization for API requests. +`; + +export const proxycurlAuth = PieceAuth.SecretText({ + displayName: 'Proxycurl API Key', + description: markdownDescription, + required: true, +}); + +export const proxycurl = createPiece({ + displayName: 'Proxycurl', + description: 'Enrich LinkedIn people and company profiles with Proxycurl.', + auth: proxycurlAuth, + minimumSupportedRelease: '0.36.1', + logoUrl: 'https://cdn.activepieces.com/pieces/proxycurl.png', + categories: [PieceCategory.SALES_AND_CRM], + authors: ['Harmatta'], + actions: [ + getPersonProfileAction, + getCompanyProfileAction, + searchPeopleAction, + lookupPersonEmailAction, + customApiCallAction, + ], + triggers: [], +}); diff --git a/packages/pieces/community/proxycurl/src/lib/actions/custom-api-call.ts b/packages/pieces/community/proxycurl/src/lib/actions/custom-api-call.ts new file mode 100644 index 00000000000..08eddd53270 --- /dev/null +++ b/packages/pieces/community/proxycurl/src/lib/actions/custom-api-call.ts @@ -0,0 +1,15 @@ +import { createCustomApiCallAction } from '@activepieces/pieces-common'; +import { proxycurlAuth } from '../../index'; +import { BASE_URL } from '../common/client'; + +export const customApiCallAction = createCustomApiCallAction({ + auth: proxycurlAuth, + name: 'custom_api_call', + displayName: 'Custom API Call', + description: 'Make a custom API call to any Proxycurl endpoint.', + baseUrl: () => BASE_URL, + authMapping: async (auth) => ({ + Authorization: `Bearer ${auth.secret_text}`, + Accept: 'application/json', + }), +}); diff --git a/packages/pieces/community/proxycurl/src/lib/actions/get-company-profile.ts b/packages/pieces/community/proxycurl/src/lib/actions/get-company-profile.ts new file mode 100644 index 00000000000..625937f3e2c --- /dev/null +++ b/packages/pieces/community/proxycurl/src/lib/actions/get-company-profile.ts @@ -0,0 +1,28 @@ +import { HttpMethod } from '@activepieces/pieces-common'; +import { createAction, Property } from '@activepieces/pieces-framework'; +import { proxycurlAuth } from '../../index'; +import { proxycurlApiCall } from '../common/client'; + +export const getCompanyProfileAction = createAction({ + name: 'get_company_profile', + displayName: 'Get Company Profile', + description: 'Fetch a LinkedIn company profile from Proxycurl.', + auth: proxycurlAuth, + props: { + url: Property.ShortText({ + displayName: 'LinkedIn Company URL', + description: 'LinkedIn company URL, for example https://www.linkedin.com/company/apple/', + required: true, + }), + }, + async run(context) { + return proxycurlApiCall({ + apiKey: context.auth.secret_text, + method: HttpMethod.GET, + resourceUri: '/linkedin/company', + query: { + url: context.propsValue.url, + }, + }); + }, +}); diff --git a/packages/pieces/community/proxycurl/src/lib/actions/get-person-profile.ts b/packages/pieces/community/proxycurl/src/lib/actions/get-person-profile.ts new file mode 100644 index 00000000000..6ea54b2f915 --- /dev/null +++ b/packages/pieces/community/proxycurl/src/lib/actions/get-person-profile.ts @@ -0,0 +1,28 @@ +import { HttpMethod } from '@activepieces/pieces-common'; +import { createAction, Property } from '@activepieces/pieces-framework'; +import { proxycurlAuth } from '../../index'; +import { proxycurlApiCall } from '../common/client'; + +export const getPersonProfileAction = createAction({ + name: 'get_person_profile', + displayName: 'Get Person Profile', + description: 'Fetch a LinkedIn person profile from Proxycurl.', + auth: proxycurlAuth, + props: { + linkedin_profile_url: Property.ShortText({ + displayName: 'LinkedIn Profile URL', + description: 'Public LinkedIn profile URL, for example https://www.linkedin.com/in/williamhgates', + required: true, + }), + }, + async run(context) { + return proxycurlApiCall({ + apiKey: context.auth.secret_text, + method: HttpMethod.GET, + resourceUri: '/v2/linkedin', + query: { + url: context.propsValue.linkedin_profile_url, + }, + }); + }, +}); diff --git a/packages/pieces/community/proxycurl/src/lib/actions/lookup-person-email.ts b/packages/pieces/community/proxycurl/src/lib/actions/lookup-person-email.ts new file mode 100644 index 00000000000..8f825497430 --- /dev/null +++ b/packages/pieces/community/proxycurl/src/lib/actions/lookup-person-email.ts @@ -0,0 +1,34 @@ +import { HttpMethod } from '@activepieces/pieces-common'; +import { createAction, Property } from '@activepieces/pieces-framework'; +import { proxycurlAuth } from '../../index'; +import { proxycurlApiCall } from '../common/client'; + +export const lookupPersonEmailAction = createAction({ + name: 'lookup_person_email', + displayName: 'Lookup Person Email', + description: 'Lookup the work email address for a LinkedIn person profile.', + auth: proxycurlAuth, + props: { + linkedin_profile_url: Property.ShortText({ + displayName: 'LinkedIn Profile URL', + description: 'Public LinkedIn profile URL to enrich.', + required: true, + }), + callback_url: Property.ShortText({ + displayName: 'Callback URL', + description: 'Optional webhook URL for async completion callbacks from Proxycurl.', + required: false, + }), + }, + async run(context) { + return proxycurlApiCall({ + apiKey: context.auth.secret_text, + method: HttpMethod.GET, + resourceUri: '/linkedin/profile/email', + query: { + linkedin_profile_url: context.propsValue.linkedin_profile_url, + callback_url: context.propsValue.callback_url, + }, + }); + }, +}); diff --git a/packages/pieces/community/proxycurl/src/lib/actions/search-people.ts b/packages/pieces/community/proxycurl/src/lib/actions/search-people.ts new file mode 100644 index 00000000000..3320ed9974c --- /dev/null +++ b/packages/pieces/community/proxycurl/src/lib/actions/search-people.ts @@ -0,0 +1,48 @@ +import { HttpMethod } from '@activepieces/pieces-common'; +import { createAction, Property } from '@activepieces/pieces-framework'; +import { proxycurlAuth } from '../../index'; +import { proxycurlApiCall } from '../common/client'; + +export const searchPeopleAction = createAction({ + name: 'search_people', + displayName: 'Search People', + description: 'Search for people in Proxycurl using lightweight keyword filters.', + auth: proxycurlAuth, + props: { + country: Property.ShortText({ + displayName: 'Country', + description: 'Optional country filter, for example us, gb, or sg.', + required: false, + }), + headline: Property.ShortText({ + displayName: 'Headline Keywords', + description: 'Optional keywords expected in the person headline.', + required: false, + }), + summary_keywords: Property.ShortText({ + displayName: 'Summary Keywords', + description: 'Optional keywords expected in the summary/about section.', + required: false, + }), + }, + async run(context) { + const { country, headline, summary_keywords } = context.propsValue; + + if (!country && !headline && !summary_keywords) { + throw new Error( + 'At least one search filter must be provided: Country, Headline Keywords, or Summary Keywords.' + ); + } + + return proxycurlApiCall({ + apiKey: context.auth.secret_text, + method: HttpMethod.GET, + resourceUri: '/v2/search/person', + query: { + country, + headline, + summary_keywords, + }, + }); + }, +}); diff --git a/packages/pieces/community/proxycurl/src/lib/common/client.ts b/packages/pieces/community/proxycurl/src/lib/common/client.ts new file mode 100644 index 00000000000..cbef226de85 --- /dev/null +++ b/packages/pieces/community/proxycurl/src/lib/common/client.ts @@ -0,0 +1,93 @@ +import { + AuthenticationType, + httpClient, + HttpMethod, + HttpRequest, + QueryParams, +} from '@activepieces/pieces-common'; + +export const BASE_URL = 'https://nubela.co/proxycurl/api'; + +type QueryValue = string | number | boolean | undefined | null; + +export type ProxycurlApiCallParams = { + apiKey: string; + method: HttpMethod; + resourceUri: string; + query?: Record; + body?: unknown; +}; + +export async function proxycurlApiCall({ + apiKey, + method, + resourceUri, + query, + body, +}: ProxycurlApiCallParams): Promise { + const queryParams: QueryParams = {}; + + if (query) { + for (const [key, value] of Object.entries(query)) { + if (value !== undefined && value !== null && value !== '') { + queryParams[key] = String(value); + } + } + } + + const request: HttpRequest = { + method, + url: `${BASE_URL}${resourceUri}`, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token: apiKey, + }, + headers: { + Accept: 'application/json', + }, + queryParams, + body, + }; + + try { + const response = await httpClient.sendRequest(request); + + if (response.status >= 400) { + const bodyMessage = + typeof response.body === 'string' + ? response.body + : JSON.stringify(response.body); + throw new Error( + `Proxycurl API error ${response.status}: ${bodyMessage}` + ); + } + + return response.body; + } catch (error: unknown) { + const proxycurlError = error as { + response?: { status?: number; body?: unknown }; + statusCode?: number; + status?: number; + body?: unknown; + message?: string; + }; + + const statusCode = + proxycurlError.response?.status ?? + proxycurlError.statusCode ?? + proxycurlError.status; + const errorBody = proxycurlError.response?.body ?? proxycurlError.body; + + if (statusCode) { + const bodyMessage = + typeof errorBody === 'string' ? errorBody : JSON.stringify(errorBody); + throw new Error( + `Proxycurl API error ${statusCode}: ${bodyMessage || proxycurlError.message || 'Unknown error'}` + ); + } + + throw new Error( + `Proxycurl request failed: ${proxycurlError.message || 'Unknown error'}` + ); + } +} diff --git a/packages/pieces/community/proxycurl/tsconfig.json b/packages/pieces/community/proxycurl/tsconfig.json new file mode 100644 index 00000000000..29c9dd1bfc1 --- /dev/null +++ b/packages/pieces/community/proxycurl/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/pieces/community/proxycurl/tsconfig.lib.json b/packages/pieces/community/proxycurl/tsconfig.lib.json new file mode 100644 index 00000000000..0ba4caeb858 --- /dev/null +++ b/packages/pieces/community/proxycurl/tsconfig.lib.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "rootDir": ".", + "baseUrl": ".", + "paths": {}, + "outDir": "./dist", + "declaration": true, + "declarationMap": true, + "types": ["node"] + }, + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts"] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index be234d24b35..66b0f9a55f4 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1964,6 +1964,9 @@ ], "@activepieces/piece-knock": [ "packages/pieces/community/knock/src/index.ts" + ], + "@activepieces/piece-proxycurl": [ + "packages/pieces/community/proxycurl/src/index.ts" ] }, "resolveJsonModule": true