diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cc2571b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +node_modules +npm-debug.log +.DS_Store +.git +.gitignore +dist +**/.vscode +**/.idea +**/*.log +coverage +.env diff --git a/.github/workflows/merge-main.yaml b/.github/workflows/merge-main.yaml new file mode 100644 index 0000000..4df90b8 --- /dev/null +++ b/.github/workflows/merge-main.yaml @@ -0,0 +1,53 @@ +name: PR Merged -> main + +on: + push: + branches: ['main'] + +jobs: + linting: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4.1.1 + + - name: Set up Node.js + uses: actions/setup-node@v4.0.1 + with: + node-version: 20 + + - name: Install dependencies + run: npm ci + + - name: Run ESLint + run: npm run lint + + build-push: + needs: [linting] + runs-on: ubuntu-latest + if: always() && needs.linting.result == 'success' + + steps: + - name: Checkout code + uses: actions/checkout@v4.1.1 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + push: true + tags: | + ghcr.io/${{ github.repository_owner }}/${{ secrets.DOCKER_IMAGE_NAME }}:prod + cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/${{ secrets.DOCKER_IMAGE_NAME }}:prod + cache-to: type=inline diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..48a1a15 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM node:20-alpine AS builder +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY tsconfig.json ./ +COPY src ./src +COPY README.md ./README.md +RUN npm run build + +FROM node:20-alpine AS runner +ENV NODE_ENV=production +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev --ignore-scripts +COPY --from=builder /app/dist ./dist + +ENV HOST=0.0.0.0 \ + PORT=3000 + +EXPOSE 3000 + +CMD ["node", "dist/index.js", "--http"] + + diff --git a/package.json b/package.json index b4aca92..4c90d17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "linkedapi-mcp", - "version": "0.2.0", + "version": "0.3.0", "description": "MCP server for Linked API", "main": "dist/index.js", "bin": { diff --git a/src/index.ts b/src/index.ts index 276a1ad..37953c4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,16 +7,27 @@ import { ListPromptsRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; +import http from 'node:http'; import { LinkedApiMCPServer } from './linked-api-server'; import { availablePrompts, getPromptContent, systemPrompt } from './prompts'; import { debugLog } from './utils/debug-log'; +import { JsonHTTPServerTransport } from './utils/json-http-transport'; import { LinkedApiProgressNotification } from './utils/types'; -async function main() { - const linkedApiToken = process.env.LINKED_API_TOKEN; - const identificationToken = process.env.IDENTIFICATION_TOKEN; +function getArgValue(flag: string): string | undefined { + const index = process.argv.indexOf(flag); + if (index === -1) return undefined; + const value = process.argv[index + 1]; + if (!value || value.startsWith('--')) return undefined; + return value; +} +function hasFlag(flag: string): boolean { + return process.argv.includes(flag); +} + +async function main() { const server = new Server( { name: 'linkedapi-mcp', @@ -33,14 +44,7 @@ async function main() { ); const progressCallback = (_notification: LinkedApiProgressNotification) => {}; - - const linkedApiServer = new LinkedApiMCPServer( - { - linkedApiToken: linkedApiToken!, - identificationToken: identificationToken!, - }, - progressCallback, - ); + const linkedApiServer = new LinkedApiMCPServer(progressCallback); server.setRequestHandler(ListToolsRequestSchema, async () => { const tools = linkedApiServer.getTools(); @@ -78,7 +82,7 @@ async function main() { } }); - server.setRequestHandler(CallToolRequestSchema, async (request) => { + server.setRequestHandler(CallToolRequestSchema, async (request, extra) => { debugLog('Tool request received', { toolName: request.params.name, arguments: request.params.arguments, @@ -86,7 +90,18 @@ async function main() { }); try { - const result = await linkedApiServer.callTool(request.params); + const localLinkedApiToken = process.env.LINKED_API_TOKEN; + const localIdentificationToken = process.env.IDENTIFICATION_TOKEN; + const headers = extra?.requestInfo?.headers ?? {}; + const linkedApiToken = (headers['linked-api-token'] ?? localLinkedApiToken ?? '') as string; + const identificationToken = (headers['identification-token'] ?? + localIdentificationToken ?? + '') as string; + + const result = await linkedApiServer.executeWithTokens(request.params, { + linkedApiToken, + identificationToken, + }); return result; } catch (error) { debugLog('Tool execution failed', { @@ -96,8 +111,52 @@ async function main() { throw error; } }); - const transport = new StdioServerTransport(); - await server.connect(transport); + + if (hasFlag('--http') || hasFlag('--transport=http')) { + const port = Number(process.env.PORT ?? getArgValue('--port') ?? 3000); + const host = process.env.HOST ?? getArgValue('--host') ?? '0.0.0.0'; + const transport = new JsonHTTPServerTransport(); + + await server.connect(transport); + + const httpServer = http.createServer(async (req, res) => { + try { + if (!req.url) { + res.statusCode = 400; + res.end('Bad Request'); + return; + } + const url = new URL(req.url, `http://${req.headers.host ?? 'localhost'}`); + // Set query parameters to headers if they are not set + const linkedApiTokenQP = url.searchParams.get('linked-api-token'); + const identificationTokenQP = url.searchParams.get('identification-token'); + if (!req.headers['linked-api-token'] && linkedApiTokenQP) { + req.headers['linked-api-token'] = linkedApiTokenQP; + } + if (!req.headers['identification-token'] && identificationTokenQP) { + req.headers['identification-token'] = identificationTokenQP; + } + await transport.handleRequest(req, res); + } catch (error) { + debugLog('HTTP request handling failed', { + error: error instanceof Error ? error.message : String(error), + }); + res.statusCode = 500; + res.end('Internal Server Error'); + } + }); + + httpServer.listen(port, host, () => { + debugLog('HTTP transport listening', { + host, + port, + }); + }); + } else { + const transport = new StdioServerTransport(); + await server.connect(transport); + debugLog('stdio transport connected'); + } } main().catch((error) => { diff --git a/src/linked-api-server.ts b/src/linked-api-server.ts index 83b1aa8..10e2a3e 100644 --- a/src/linked-api-server.ts +++ b/src/linked-api-server.ts @@ -1,14 +1,10 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import { - LinkedApi, - LinkedApiError, - LinkedApiWorkflowTimeoutError, - TLinkedApiConfig, -} from 'linkedapi-node'; +import { LinkedApi, LinkedApiError, TLinkedApiConfig } from 'linkedapi-node'; import { buildLinkedApiHttpClient } from 'linkedapi-node/dist/core'; import { LinkedApiTools } from './linked-api-tools'; import { debugLog } from './utils/debug-log'; +import { handleLinkedApiError } from './utils/handle-linked-api-error'; import { CallToolResult, ExtendedCallToolRequest, @@ -16,15 +12,21 @@ import { } from './utils/types'; export class LinkedApiMCPServer { - private linkedapi: LinkedApi; private tools: LinkedApiTools; - private progressCallback: (notification: LinkedApiProgressNotification) => void; - constructor( + constructor(progressCallback: (notification: LinkedApiProgressNotification) => void) { + this.tools = new LinkedApiTools(progressCallback); + } + + public getTools(): Tool[] { + return this.tools.tools.map((tool) => tool.getTool()); + } + + public async executeWithTokens( + request: ExtendedCallToolRequest['params'], config: TLinkedApiConfig, - progressCallback: (notification: LinkedApiProgressNotification) => void, - ) { - this.linkedapi = new LinkedApi( + ): Promise { + const linkedApi = new LinkedApi( buildLinkedApiHttpClient( { linkedApiToken: config.linkedApiToken!, @@ -33,26 +35,14 @@ export class LinkedApiMCPServer { 'mcp', ), ); - this.progressCallback = progressCallback; - this.tools = new LinkedApiTools(this.linkedapi, this.progressCallback); - } - - public getTools(): Tool[] { - return [...this.tools.tools.map((t) => t.getTool())]; - } - - public async callTool(request: ExtendedCallToolRequest['params']): Promise { const { name, arguments: args, _meta } = request; const progressToken = _meta?.progressToken; try { - const tool = this.tools.toolByName(name); - if (!tool) { - throw new Error(`Unknown tool: ${name}`); - } + const tool = this.tools.toolByName(name)!; const params = tool.validate(args); - const { data, errors } = await tool.execute(params, progressToken); + const { data, errors } = await tool.execute(linkedApi, params, progressToken); if (errors.length > 0 && !data) { return { content: [ @@ -73,15 +63,7 @@ export class LinkedApiMCPServer { }; } catch (error) { if (error instanceof LinkedApiError) { - let body: unknown = error; - if (error instanceof LinkedApiWorkflowTimeoutError) { - const { message, workflowId, operationName } = error; - body = { - message, - workflowId, - operationName, - }; - } + const body = handleLinkedApiError(error); return { content: [ { diff --git a/src/linked-api-tools.ts b/src/linked-api-tools.ts index 87333e9..7b60f2d 100644 --- a/src/linked-api-tools.ts +++ b/src/linked-api-tools.ts @@ -1,5 +1,3 @@ -import LinkedApi from 'linkedapi-node'; - import { CheckConnectionStatusTool } from './tools/check-connection-status.js'; import { CommentOnPostTool } from './tools/comment-on-post.js'; import { ExecuteCustomWorkflowTool } from './tools/execute-custom-workflow.js'; @@ -32,40 +30,37 @@ import { LinkedApiProgressNotification } from './utils/types.js'; export class LinkedApiTools { public readonly tools: ReadonlyArray>; - constructor( - linkedapi: LinkedApi, - progressCallback: (progress: LinkedApiProgressNotification) => void, - ) { + constructor(progressCallback: (progress: LinkedApiProgressNotification) => void) { this.tools = [ // Standard tools - new SendMessageTool(linkedapi, progressCallback), - new GetConversationTool(linkedapi, progressCallback), - new CheckConnectionStatusTool(linkedapi, progressCallback), - new RetrieveConnectionsTool(linkedapi, progressCallback), - new SendConnectionRequestTool(linkedapi, progressCallback), - new WithdrawConnectionRequestTool(linkedapi, progressCallback), - new RetrievePendingRequestsTool(linkedapi, progressCallback), - new RemoveConnectionTool(linkedapi, progressCallback), - new SearchCompaniesTool(linkedapi, progressCallback), - new SearchPeopleTool(linkedapi, progressCallback), - new FetchCompanyTool(linkedapi, progressCallback), - new FetchPersonTool(linkedapi, progressCallback), - new FetchPostTool(linkedapi, progressCallback), - new ReactToPostTool(linkedapi, progressCallback), - new CommentOnPostTool(linkedapi, progressCallback), - new RetrieveSSITool(linkedapi, progressCallback), - new RetrievePerformanceTool(linkedapi, progressCallback), + new SendMessageTool(progressCallback), + new GetConversationTool(progressCallback), + new CheckConnectionStatusTool(progressCallback), + new RetrieveConnectionsTool(progressCallback), + new SendConnectionRequestTool(progressCallback), + new WithdrawConnectionRequestTool(progressCallback), + new RetrievePendingRequestsTool(progressCallback), + new RemoveConnectionTool(progressCallback), + new SearchCompaniesTool(progressCallback), + new SearchPeopleTool(progressCallback), + new FetchCompanyTool(progressCallback), + new FetchPersonTool(progressCallback), + new FetchPostTool(progressCallback), + new ReactToPostTool(progressCallback), + new CommentOnPostTool(progressCallback), + new RetrieveSSITool(progressCallback), + new RetrievePerformanceTool(progressCallback), // Sales Navigator tools - new NvSendMessageTool(linkedapi, progressCallback), - new NvGetConversationTool(linkedapi, progressCallback), - new NvSearchCompaniesTool(linkedapi, progressCallback), - new NvSearchPeopleTool(linkedapi, progressCallback), - new NvFetchCompanyTool(linkedapi, progressCallback), - new NvFetchPersonTool(linkedapi, progressCallback), + new NvSendMessageTool(progressCallback), + new NvGetConversationTool(progressCallback), + new NvSearchCompaniesTool(progressCallback), + new NvSearchPeopleTool(progressCallback), + new NvFetchCompanyTool(progressCallback), + new NvFetchPersonTool(progressCallback), // Other tools - new ExecuteCustomWorkflowTool(linkedapi, progressCallback), - new GetWorkflowResultTool(linkedapi, progressCallback), - new GetApiUsageTool(linkedapi, progressCallback), + new ExecuteCustomWorkflowTool(progressCallback), + new GetWorkflowResultTool(progressCallback), + new GetApiUsageTool(progressCallback), ]; } diff --git a/src/prompts/index.ts b/src/prompts/index.ts index 8183217..c28a800 100644 --- a/src/prompts/index.ts +++ b/src/prompts/index.ts @@ -9,6 +9,20 @@ DEFAULT BEHAVIOR: Always use basic requests first. For example: DON'T enable multiple optional flags unless the user specifically requests all that data. +AUTHENTICATION ERROR HANDLING + +WHEN YOU GET AN AUTHENTICATION ERROR: +The MCP client must provide valid authentication tokens to use Linked API services. + +REQUIRED TOKENS (have to be provided in headers in each request): +- linked-api-token: Your Linked API access token from linkedapi.io +- identification-token: Your identification token from linkedapi.io + +TOKEN SETUP: +1. Visit https://linkedapi.io to get tokens +2. Set headers with every request +3. Keep tokens secure and never share them in logs or error messages + BACKGROUND WORKFLOW HANDLING LINKEDIN WORKFLOWS RUN IN THE BACKGROUND: @@ -129,6 +143,18 @@ COMMON MISTAKES TO AVOID: - Omitting operationName (it is required) - Treating the background message as a failure instead of an active process`; +export const authenticationPrompt = `AUTHENTICATION REQUIREMENTS: +- MCP clients must provide valid authentication tokens to use Linked API services +- linked-api-token: Main Linked API access token from linkedapi.io +- identification-token: Account identification token from linkedapi.io + +TOKEN SETUP: +1. Visit https://linkedapi.io to get tokens +2. Set headers with every request in MCP client configuration +3. Keep tokens secure and never share them in logs or error messages + +More information: https://linkedapi.io/mcp/installation/`; + export const availablePrompts = [ { name: 'performance_guidelines', @@ -142,6 +168,10 @@ export const availablePrompts = [ name: 'background_workflows', description: 'Learn how Linked API background workflows provide optimal UX', }, + { + name: 'authentication_requirements', + description: 'Learn how to authenticate with Linked API MCP', + }, ]; export function getPromptContent(name: string): string { @@ -152,6 +182,8 @@ export function getPromptContent(name: string): string { return parameterUsageGuidelines; case 'background_workflows': return backgroundWorkflowPrompt; + case 'authentication_requirements': + return authenticationPrompt; default: throw new Error(`Unknown prompt: ${name}`); } diff --git a/src/tools/check-connection-status.ts b/src/tools/check-connection-status.ts index 97b37be..d069c7c 100644 --- a/src/tools/check-connection-status.ts +++ b/src/tools/check-connection-status.ts @@ -1,29 +1,23 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi, { +import { + OPERATION_NAME, TCheckConnectionStatusParams, TCheckConnectionStatusResult, } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class CheckConnectionStatusTool extends OperationTool< TCheckConnectionStatusParams, TCheckConnectionStatusResult > { public override readonly name = 'check_connection_status'; + public override readonly operationName = OPERATION_NAME.checkConnectionStatus; protected override readonly schema = z.object({ personUrl: z.string(), }); - constructor( - linkedapi: LinkedApi, - progressCallback: (progress: LinkedApiProgressNotification) => void, - ) { - super(linkedapi.checkConnectionStatus, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/comment-on-post.ts b/src/tools/comment-on-post.ts index 8424a74..3e24bfd 100644 --- a/src/tools/comment-on-post.ts +++ b/src/tools/comment-on-post.ts @@ -1,24 +1,17 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi, { TCommentOnPostParams } from 'linkedapi-node'; +import { OPERATION_NAME, TCommentOnPostParams } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class CommentOnPostTool extends OperationTool { public override readonly name = 'comment_on_post'; + public override readonly operationName = OPERATION_NAME.commentOnPost; protected override readonly schema = z.object({ postUrl: z.string(), text: z.string().min(1), }); - constructor( - linkedapi: LinkedApi, - progressCallback: (progress: LinkedApiProgressNotification) => void, - ) { - super(linkedapi.commentOnPost, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/execute-custom-workflow.ts b/src/tools/execute-custom-workflow.ts index b688d04..efa0113 100644 --- a/src/tools/execute-custom-workflow.ts +++ b/src/tools/execute-custom-workflow.ts @@ -1,26 +1,19 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi, { TWorkflowCompletion, TWorkflowDefinition } from 'linkedapi-node'; +import { OPERATION_NAME, TWorkflowCompletion, TWorkflowDefinition } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class ExecuteCustomWorkflowTool extends OperationTool< TWorkflowDefinition, TWorkflowCompletion > { public override readonly name = 'execute_custom_workflow'; + public override readonly operationName = OPERATION_NAME.customWorkflow; protected override readonly schema = z.object({ definition: z.any(), }); - constructor( - linkedapi: LinkedApi, - progressCallback: (progress: LinkedApiProgressNotification) => void, - ) { - super(linkedapi.customWorkflow, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/fetch-company.ts b/src/tools/fetch-company.ts index 7fba586..5ab6b5f 100644 --- a/src/tools/fetch-company.ts +++ b/src/tools/fetch-company.ts @@ -1,12 +1,12 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi, { TFetchCompanyParams } from 'linkedapi-node'; +import { OPERATION_NAME, TFetchCompanyParams } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class FetchCompanyTool extends OperationTool { public override readonly name = 'fetch_company'; + public override readonly operationName = OPERATION_NAME.fetchCompany; protected override readonly schema = z.object({ companyUrl: z.string(), retrieveEmployees: z.boolean().optional().default(false), @@ -40,13 +40,6 @@ export class FetchCompanyTool extends OperationTool void, - ) { - super(linkedapi.fetchCompany, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/fetch-person.ts b/src/tools/fetch-person.ts index 0bc5b59..0d86512 100644 --- a/src/tools/fetch-person.ts +++ b/src/tools/fetch-person.ts @@ -1,12 +1,12 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi, { TFetchPersonParams } from 'linkedapi-node'; +import { OPERATION_NAME, TFetchPersonParams } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class FetchPersonTool extends OperationTool { public override readonly name = 'fetch_person'; + public override readonly operationName = OPERATION_NAME.fetchPerson; protected override readonly schema = z.object({ personUrl: z.string(), retrieveExperience: z.boolean().optional().default(false), @@ -36,13 +36,6 @@ export class FetchPersonTool extends OperationTool .optional(), }); - constructor( - linkedapi: LinkedApi, - progressCallback: (progress: LinkedApiProgressNotification) => void, - ) { - super(linkedapi.fetchPerson, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/fetch-post.ts b/src/tools/fetch-post.ts index fb996df..0847922 100644 --- a/src/tools/fetch-post.ts +++ b/src/tools/fetch-post.ts @@ -1,23 +1,16 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi, { TFetchPostParams } from 'linkedapi-node'; +import { OPERATION_NAME, TFetchPostParams } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class FetchPostTool extends OperationTool { public override readonly name = 'fetch_post'; + public override readonly operationName = OPERATION_NAME.fetchPost; protected override readonly schema = z.object({ postUrl: z.string(), }); - constructor( - linkedapi: LinkedApi, - progressCallback: (progress: LinkedApiProgressNotification) => void, - ) { - super(linkedapi.fetchPost, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/get-api-usage-stats.ts b/src/tools/get-api-usage-stats.ts index 01883ea..0e5f4c4 100644 --- a/src/tools/get-api-usage-stats.ts +++ b/src/tools/get-api-usage-stats.ts @@ -3,7 +3,6 @@ import LinkedApi, { TApiUsageAction, TApiUsageParams, TMappedResponse } from 'li import z from 'zod'; import { LinkedApiTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class GetApiUsageTool extends LinkedApiTool { public readonly name = 'get_api_usage'; @@ -12,19 +11,11 @@ export class GetApiUsageTool extends LinkedApiTool void, - ) { - super(progressCallback); - this.linkedapi = linkedapi; - } - public override async execute( + linkedapi: LinkedApi, args: TApiUsageParams, ): Promise> { - return await this.linkedapi.getApiUsage(args); + return await linkedapi.getApiUsage(args); } public override getTool(): Tool { diff --git a/src/tools/get-conversation.ts b/src/tools/get-conversation.ts index 62347e8..4464272 100644 --- a/src/tools/get-conversation.ts +++ b/src/tools/get-conversation.ts @@ -3,7 +3,6 @@ import LinkedApi, { TConversationPollResult, TMappedResponse } from 'linkedapi-n import z from 'zod'; import { LinkedApiTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class GetConversationTool extends LinkedApiTool< { personUrl: string; since?: string }, @@ -15,37 +14,31 @@ export class GetConversationTool extends LinkedApiTool< since: z.string().optional(), }); - private readonly linkedapi: LinkedApi; - - constructor( + public override async execute( linkedapi: LinkedApi, - progressCallback: (progress: LinkedApiProgressNotification) => void, - ) { - super(progressCallback); - this.linkedapi = linkedapi; - } - - public override async execute({ - personUrl, - since, - }: { - personUrl: string; - since?: string; - }): Promise> { - const conversations = await this.getConversation(personUrl, since); + { + personUrl, + since, + }: { + personUrl: string; + since?: string; + }, + ): Promise> { + const conversations = await this.getConversation(linkedapi, personUrl, since); if (conversations.errors.length === 0) { return conversations; } - const workflowId = await this.linkedapi.syncConversation.execute({ personUrl }); - await this.linkedapi.syncConversation.result(workflowId); - return await this.getConversation(personUrl, since); + const workflowId = await linkedapi.syncConversation.execute({ personUrl }); + await linkedapi.syncConversation.result(workflowId); + return await this.getConversation(linkedapi, personUrl, since); } private async getConversation( + linkedapi: LinkedApi, personUrl: string, since?: string, ): Promise> { - const conversations = await this.linkedapi.pollConversations([ + const conversations = await linkedapi.pollConversations([ { personUrl: personUrl, type: 'st', diff --git a/src/tools/get-workflow-result.ts b/src/tools/get-workflow-result.ts index 524deff..198eba4 100644 --- a/src/tools/get-workflow-result.ts +++ b/src/tools/get-workflow-result.ts @@ -4,7 +4,6 @@ import z from 'zod'; import { executeWithProgress } from '../utils/execute-with-progress.js'; import { LinkedApiTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; interface IGetWorkflowResultParams { workflowId: string; @@ -18,26 +17,14 @@ export class GetWorkflowResultTool extends LinkedApiTool void, - ) { - super(progressCallback); - this.linkedapi = linkedapi; - } - public override async execute( + linkedapi: LinkedApi, args: IGetWorkflowResultParams, progressToken?: string | number, ): Promise> { - const operation = this.linkedapi.operations.find( + const operation = linkedapi.operations.find( (operation) => operation.operationName === args.operationName, - ); - if (!operation) { - throw new Error(`Operation ${args.operationName} not found`); - } + )!; return await executeWithProgress(this.progressCallback, operation, { workflowId: args.workflowId, progressToken, diff --git a/src/tools/nv-fetch-company.ts b/src/tools/nv-fetch-company.ts index 2ff4ac1..9e96b49 100644 --- a/src/tools/nv-fetch-company.ts +++ b/src/tools/nv-fetch-company.ts @@ -1,12 +1,12 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi, { TNvFetchCompanyParams } from 'linkedapi-node'; +import { OPERATION_NAME, TNvFetchCompanyParams } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class NvFetchCompanyTool extends OperationTool { public override readonly name = 'nv_fetch_company'; + public override readonly operationName = OPERATION_NAME.nvFetchCompany; protected override readonly schema = z.object({ companyHashedUrl: z.string(), retrieveEmployees: z.boolean().optional().default(false), @@ -34,13 +34,6 @@ export class NvFetchCompanyTool extends OperationTool void, - ) { - super(linkedapi.nvFetchCompany, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/nv-fetch-person.ts b/src/tools/nv-fetch-person.ts index 33e671c..dc90f6d 100644 --- a/src/tools/nv-fetch-person.ts +++ b/src/tools/nv-fetch-person.ts @@ -1,23 +1,16 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi, { TNvOpenPersonPageParams } from 'linkedapi-node'; +import { OPERATION_NAME, TNvOpenPersonPageParams } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class NvFetchPersonTool extends OperationTool { public override readonly name = 'nv_fetch_person'; + public override readonly operationName = OPERATION_NAME.nvFetchPerson; protected override readonly schema = z.object({ personHashedUrl: z.string(), }); - constructor( - linkedapi: LinkedApi, - progressCallback: (progress: LinkedApiProgressNotification) => void, - ) { - super(linkedapi.nvFetchPerson, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/nv-get-conversation.ts b/src/tools/nv-get-conversation.ts index 4599465..e2b90fc 100644 --- a/src/tools/nv-get-conversation.ts +++ b/src/tools/nv-get-conversation.ts @@ -3,7 +3,6 @@ import LinkedApi, { TConversationPollResult, TMappedResponse } from 'linkedapi-n import z from 'zod'; import { LinkedApiTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class NvGetConversationTool extends LinkedApiTool< { personUrl: string; since?: string }, @@ -15,37 +14,31 @@ export class NvGetConversationTool extends LinkedApiTool< since: z.string().optional(), }); - private readonly linkedapi: LinkedApi; - - constructor( + public override async execute( linkedapi: LinkedApi, - progressCallback: (progress: LinkedApiProgressNotification) => void, - ) { - super(progressCallback); - this.linkedapi = linkedapi; - } - - public override async execute({ - personUrl, - since, - }: { - personUrl: string; - since?: string; - }): Promise> { - const conversations = await this.getConversation(personUrl, since); + { + personUrl, + since, + }: { + personUrl: string; + since?: string; + }, + ): Promise> { + const conversations = await this.getConversation(linkedapi, personUrl, since); if (conversations.errors.length === 0) { return conversations; } - const workflowId = await this.linkedapi.nvSyncConversation.execute({ personUrl }); - await this.linkedapi.nvSyncConversation.result(workflowId); - return await this.getConversation(personUrl, since); + const workflowId = await linkedapi.nvSyncConversation.execute({ personUrl }); + await linkedapi.nvSyncConversation.result(workflowId); + return await this.getConversation(linkedapi, personUrl, since); } private async getConversation( + linkedapi: LinkedApi, personUrl: string, since?: string, ): Promise> { - const conversations = await this.linkedapi.pollConversations([ + const conversations = await linkedapi.pollConversations([ { personUrl: personUrl, type: 'nv', diff --git a/src/tools/nv-search-companies.ts b/src/tools/nv-search-companies.ts index 1297e17..05968dd 100644 --- a/src/tools/nv-search-companies.ts +++ b/src/tools/nv-search-companies.ts @@ -1,12 +1,12 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi, { TNvSearchCompaniesParams } from 'linkedapi-node'; +import { OPERATION_NAME, TNvSearchCompaniesParams } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class NvSearchCompaniesTool extends OperationTool { public override readonly name = 'nv_search_companies'; + public override readonly operationName = OPERATION_NAME.nvSearchCompanies; protected override readonly schema = z.object({ term: z.string().optional(), limit: z.number().min(1).max(100).optional(), @@ -38,13 +38,6 @@ export class NvSearchCompaniesTool extends OperationTool void, - ) { - super(linkedapi.nvSearchCompanies, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/nv-search-people.ts b/src/tools/nv-search-people.ts index a71ae65..031ce7a 100644 --- a/src/tools/nv-search-people.ts +++ b/src/tools/nv-search-people.ts @@ -1,12 +1,12 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi, { TNvSearchPeopleParams } from 'linkedapi-node'; +import { OPERATION_NAME, TNvSearchPeopleParams } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class NvSearchPeopleTool extends OperationTool { public override readonly name = 'nv_search_people'; + public override readonly operationName = OPERATION_NAME.nvSearchPeople; protected override readonly schema = z.object({ term: z.string().optional(), limit: z.number().min(1).max(100).optional(), @@ -25,13 +25,6 @@ export class NvSearchPeopleTool extends OperationTool void, - ) { - super(linkedapi.nvSearchPeople, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/nv-send-message.ts b/src/tools/nv-send-message.ts index 62a68b0..70d8061 100644 --- a/src/tools/nv-send-message.ts +++ b/src/tools/nv-send-message.ts @@ -1,25 +1,18 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi, { TNvSendMessageParams } from 'linkedapi-node'; +import { OPERATION_NAME, TNvSendMessageParams } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class NvSendMessageTool extends OperationTool { public override readonly name = 'nv_send_message'; + public override readonly operationName = OPERATION_NAME.nvSendMessage; protected override readonly schema = z.object({ personUrl: z.string(), text: z.string().min(1), subject: z.string().optional(), }); - constructor( - linkedapi: LinkedApi, - progressCallback: (progress: LinkedApiProgressNotification) => void, - ) { - super(linkedapi.nvSendMessage, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/nv-sync-conversation.ts b/src/tools/nv-sync-conversation.ts index 774bb01..9b17e71 100644 --- a/src/tools/nv-sync-conversation.ts +++ b/src/tools/nv-sync-conversation.ts @@ -1,23 +1,16 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi, { TNvSyncConversationParams } from 'linkedapi-node'; +import { OPERATION_NAME, TNvSyncConversationParams } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class NvSyncConversationTool extends OperationTool { public override readonly name = 'nv_sync_conversation'; + public override readonly operationName = OPERATION_NAME.nvSyncConversation; protected override readonly schema = z.object({ personUrl: z.string(), }); - constructor( - linkedapi: LinkedApi, - progressCallback: (progress: LinkedApiProgressNotification) => void, - ) { - super(linkedapi.nvSyncConversation, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/react-to-post.ts b/src/tools/react-to-post.ts index dde206b..558dba0 100644 --- a/src/tools/react-to-post.ts +++ b/src/tools/react-to-post.ts @@ -1,24 +1,17 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi, { TReactToPostParams } from 'linkedapi-node'; +import { OPERATION_NAME, TReactToPostParams } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class ReactToPostTool extends OperationTool { public override readonly name = 'react_to_post'; + public override readonly operationName = OPERATION_NAME.reactToPost; protected override readonly schema = z.object({ postUrl: z.string(), type: z.enum(['like', 'love', 'celebrate', 'support', 'funny', 'insightful']).or(z.string()), }); - constructor( - linkedapi: LinkedApi, - progressCallback: (progress: LinkedApiProgressNotification) => void, - ) { - super(linkedapi.reactToPost, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/remove-connection.ts b/src/tools/remove-connection.ts index 3adf717..4588807 100644 --- a/src/tools/remove-connection.ts +++ b/src/tools/remove-connection.ts @@ -1,23 +1,16 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi, { TRemoveConnectionParams } from 'linkedapi-node'; +import { OPERATION_NAME, TRemoveConnectionParams } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class RemoveConnectionTool extends OperationTool { public override readonly name = 'remove_connection'; + public override readonly operationName = OPERATION_NAME.removeConnection; protected override readonly schema = z.object({ personUrl: z.string(), }); - constructor( - linkedapi: LinkedApi, - progressCallback: (progress: LinkedApiProgressNotification) => void, - ) { - super(linkedapi.removeConnection, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/retrieve-connections.ts b/src/tools/retrieve-connections.ts index 7791b8c..2adb38c 100644 --- a/src/tools/retrieve-connections.ts +++ b/src/tools/retrieve-connections.ts @@ -1,15 +1,19 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi, { TRetrieveConnectionsParams, TRetrieveConnectionsResult } from 'linkedapi-node'; +import { + OPERATION_NAME, + TRetrieveConnectionsParams, + TRetrieveConnectionsResult, +} from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class RetrieveConnectionsTool extends OperationTool< TRetrieveConnectionsParams, TRetrieveConnectionsResult[] > { public override readonly name = 'retrieve_connections'; + public override readonly operationName = OPERATION_NAME.retrieveConnections; protected override readonly schema = z.object({ limit: z.number().min(1).max(100).optional(), filter: z @@ -26,13 +30,6 @@ export class RetrieveConnectionsTool extends OperationTool< .optional(), }); - constructor( - linkedapi: LinkedApi, - progressCallback: (progress: LinkedApiProgressNotification) => void, - ) { - super(linkedapi.retrieveConnections, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/retrieve-pending-requests.ts b/src/tools/retrieve-pending-requests.ts index 7220655..d3f0453 100644 --- a/src/tools/retrieve-pending-requests.ts +++ b/src/tools/retrieve-pending-requests.ts @@ -1,21 +1,14 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi from 'linkedapi-node'; +import { OPERATION_NAME } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class RetrievePendingRequestsTool extends OperationTool { public override readonly name = 'retrieve_pending_requests'; + public override readonly operationName = OPERATION_NAME.retrievePendingRequests; protected override readonly schema = z.object({}); - constructor( - linkedapi: LinkedApi, - progressCallback: (progress: LinkedApiProgressNotification) => void, - ) { - super(linkedapi.retrievePendingRequests, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/retrieve-performance.ts b/src/tools/retrieve-performance.ts index 637e686..a613726 100644 --- a/src/tools/retrieve-performance.ts +++ b/src/tools/retrieve-performance.ts @@ -1,21 +1,14 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi from 'linkedapi-node'; +import { OPERATION_NAME } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class RetrievePerformanceTool extends OperationTool { public override readonly name = 'retrieve_performance'; + public override readonly operationName = OPERATION_NAME.retrievePerformance; protected override readonly schema = z.object({}); - constructor( - linkedapi: LinkedApi, - progressCallback: (progress: LinkedApiProgressNotification) => void, - ) { - super(linkedapi.retrievePerformance, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/retrieve-ssi.ts b/src/tools/retrieve-ssi.ts index 9ae6798..016f73b 100644 --- a/src/tools/retrieve-ssi.ts +++ b/src/tools/retrieve-ssi.ts @@ -1,21 +1,14 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi from 'linkedapi-node'; +import { OPERATION_NAME } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class RetrieveSSITool extends OperationTool { + public override readonly operationName = OPERATION_NAME.retrieveSSI; public override readonly name = 'retrieve_ssi'; protected override readonly schema = z.object({}); - constructor( - linkedapi: LinkedApi, - progressCallback: (progress: LinkedApiProgressNotification) => void, - ) { - super(linkedapi.retrieveSSI, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/search-companies.ts b/src/tools/search-companies.ts index c00e311..a848f64 100644 --- a/src/tools/search-companies.ts +++ b/src/tools/search-companies.ts @@ -1,12 +1,12 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi, { TSearchCompaniesParams } from 'linkedapi-node'; +import { OPERATION_NAME, TSearchCompaniesParams } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class SearchCompaniesTool extends OperationTool { public override readonly name = 'search_companies'; + public override readonly operationName = OPERATION_NAME.searchCompanies; protected override readonly schema = z.object({ term: z.string().optional(), limit: z.number().min(1).max(100).optional(), @@ -32,13 +32,6 @@ export class SearchCompaniesTool extends OperationTool void, - ) { - super(linkedapi.searchCompanies, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/search-people.ts b/src/tools/search-people.ts index 31b1f12..17a2b4e 100644 --- a/src/tools/search-people.ts +++ b/src/tools/search-people.ts @@ -1,12 +1,12 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi, { TSearchPeopleParams } from 'linkedapi-node'; +import { OPERATION_NAME, TSearchPeopleParams } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class SearchPeopleTool extends OperationTool { public override readonly name = 'search_people'; + public override readonly operationName = OPERATION_NAME.searchPeople; protected override readonly schema = z.object({ term: z.string().optional(), limit: z.number().min(1).max(100).optional(), @@ -24,13 +24,6 @@ export class SearchPeopleTool extends OperationTool void, - ) { - super(linkedapi.searchPeople, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/send-connection-request.ts b/src/tools/send-connection-request.ts index 483ef6f..ae87d71 100644 --- a/src/tools/send-connection-request.ts +++ b/src/tools/send-connection-request.ts @@ -1,28 +1,21 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi, { TSendConnectionRequestParams } from 'linkedapi-node'; +import { OPERATION_NAME, TSendConnectionRequestParams } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class SendConnectionRequestTool extends OperationTool< TSendConnectionRequestParams, unknown > { public override readonly name = 'send_connection_request'; + public override readonly operationName = OPERATION_NAME.sendConnectionRequest; protected override readonly schema = z.object({ personUrl: z.string(), note: z.string().optional(), email: z.string().optional(), }); - constructor( - linkedapi: LinkedApi, - progressCallback: (progress: LinkedApiProgressNotification) => void, - ) { - super(linkedapi.sendConnectionRequest, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/send-message.ts b/src/tools/send-message.ts index e3c51c9..853c9c8 100644 --- a/src/tools/send-message.ts +++ b/src/tools/send-message.ts @@ -1,24 +1,17 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi, { TSendMessageParams } from 'linkedapi-node'; +import { OPERATION_NAME, TSendMessageParams } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class SendMessageTool extends OperationTool { public override readonly name = 'send_message'; + public override readonly operationName = OPERATION_NAME.sendMessage; protected override readonly schema = z.object({ personUrl: z.string(), text: z.string().min(1), }); - constructor( - linkedapi: LinkedApi, - progressCallback: (progress: LinkedApiProgressNotification) => void, - ) { - super(linkedapi.sendMessage, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/sync-conversation.ts b/src/tools/sync-conversation.ts index 30dea3b..2bf580e 100644 --- a/src/tools/sync-conversation.ts +++ b/src/tools/sync-conversation.ts @@ -1,23 +1,16 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi, { TSyncConversationParams } from 'linkedapi-node'; +import { OPERATION_NAME, TSyncConversationParams } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class SyncConversationTool extends OperationTool { public override readonly name = 'sync_conversation'; + public override readonly operationName = OPERATION_NAME.syncConversation; protected override readonly schema = z.object({ personUrl: z.string(), }); - constructor( - linkedapi: LinkedApi, - progressCallback: (progress: LinkedApiProgressNotification) => void, - ) { - super(linkedapi.syncConversation, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/tools/withdraw-connection-request.ts b/src/tools/withdraw-connection-request.ts index 98ceea7..adc90d9 100644 --- a/src/tools/withdraw-connection-request.ts +++ b/src/tools/withdraw-connection-request.ts @@ -1,27 +1,20 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import LinkedApi, { TWithdrawConnectionRequestParams } from 'linkedapi-node'; +import { OPERATION_NAME, TWithdrawConnectionRequestParams } from 'linkedapi-node'; import { z } from 'zod'; import { OperationTool } from '../utils/linked-api-tool.js'; -import { LinkedApiProgressNotification } from '../utils/types.js'; export class WithdrawConnectionRequestTool extends OperationTool< TWithdrawConnectionRequestParams, unknown > { public override readonly name = 'withdraw_connection_request'; + public override readonly operationName = OPERATION_NAME.withdrawConnectionRequest; protected override readonly schema = z.object({ personUrl: z.string(), unfollow: z.boolean().optional(), }); - constructor( - linkedapi: LinkedApi, - progressCallback: (progress: LinkedApiProgressNotification) => void, - ) { - super(linkedapi.withdrawConnectionRequest, progressCallback); - } - public override getTool(): Tool { return { name: this.name, diff --git a/src/utils/execute-with-progress.ts b/src/utils/execute-with-progress.ts index d8a5b48..dba9752 100644 --- a/src/utils/execute-with-progress.ts +++ b/src/utils/execute-with-progress.ts @@ -11,7 +11,7 @@ export async function executeWithProgress( progressToken, }: { params?: TParams; workflowId?: string; progressToken?: string | number } = {}, ): Promise> { - const workflowTimeout = parseInt(process.env.HEALTH_CHECK_PERIOD || '60', 10) * 1000; + const workflowTimeout = parseInt(process.env.HEALTH_CHECK_PERIOD || '180', 10) * 1000; let progress = 0; progressCallback({ diff --git a/src/utils/handle-linked-api-error.ts b/src/utils/handle-linked-api-error.ts new file mode 100644 index 0000000..81caa9f --- /dev/null +++ b/src/utils/handle-linked-api-error.ts @@ -0,0 +1,27 @@ +import { LinkedApiError, LinkedApiWorkflowTimeoutError } from 'linkedapi-node'; + +import { authenticationPrompt } from '../prompts'; + +export function handleLinkedApiError(error: LinkedApiError): object { + if (error instanceof LinkedApiWorkflowTimeoutError) { + const { message, workflowId, operationName } = error; + return { + message, + workflowId, + operationName, + }; + } + switch (error.type) { + case 'identificationTokenRequired': + case 'linkedApiTokenRequired': + case 'invalidLinkedApiToken': + case 'invalidIdentificationToken': + return { + message: authenticationPrompt, + }; + } + return { + message: error.message, + type: error.type, + }; +} diff --git a/src/utils/json-http-transport.ts b/src/utils/json-http-transport.ts new file mode 100644 index 0000000..92e948c --- /dev/null +++ b/src/utils/json-http-transport.ts @@ -0,0 +1,206 @@ +import { Transport, TransportSendOptions } from '@modelcontextprotocol/sdk/shared/transport.js'; +import { + isJSONRPCError, + isJSONRPCRequest, + isJSONRPCResponse, + JSONRPCMessage, + JSONRPCMessageSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import { IncomingMessage, ServerResponse } from 'node:http'; + +type RequestId = number | string; + +type ConnectionContext = { + res: ServerResponse; + orderedIds: RequestId[]; + pendingIds: Set; + responses: Map; +}; + +export class JsonHTTPServerTransport implements Transport { + public onclose?: () => void; + public onerror?: (error: Error) => void; + public onmessage?: ( + message: JSONRPCMessage, + extra?: { requestInfo?: { headers: IncomingMessage['headers'] }; authInfo?: unknown }, + ) => void; + + private started = false; + private requestIdToConn = new Map(); + private connections = new Map(); + + async start(): Promise { + if (this.started) throw new Error('Transport already started'); + this.started = true; + } + + async close(): Promise { + this.connections.forEach((ctx) => { + try { + if (!ctx.res.writableEnded) { + ctx.res.end(); + } + } catch { + // ignore + } + }); + this.connections.clear(); + this.requestIdToConn.clear(); + this.onclose?.(); + } + + async send(message: JSONRPCMessage, options?: TransportSendOptions): Promise { + let relatedId = options?.relatedRequestId; + if (isJSONRPCResponse(message) || isJSONRPCError(message)) { + relatedId = message.id; + } + if (relatedId === undefined) { + // No place to send notifications/responses without a related request in JSON-only mode + return; + } + const connId = this.requestIdToConn.get(relatedId); + if (!connId) throw new Error(`No HTTP connection for request ${String(relatedId)}`); + const ctx = this.connections.get(connId); + if (!ctx) throw new Error(`HTTP connection closed for request ${String(relatedId)}`); + + ctx.responses.set(relatedId, message); + // When all responses for this HTTP request are ready, flush JSON and end + const allReady = ctx.orderedIds.every((id) => ctx.responses.has(id)); + if (!allReady) return; + + const body = + ctx.orderedIds.length === 1 + ? ctx.responses.get(ctx.orderedIds[0]!) + : ctx.orderedIds.map((id) => ctx.responses.get(id)); + + const headers: Record = { 'Content-Type': 'application/json' }; + ctx.res.writeHead(200, headers); + ctx.res.end(JSON.stringify(body)); + + // Cleanup + this.connections.delete(connId); + ctx.orderedIds.forEach((id) => this.requestIdToConn.delete(id)); + } + + // Handle only POST requests; no SSE/GET support + async handleRequest( + req: IncomingMessage & { auth?: unknown }, + res: ServerResponse, + parsedBody?: unknown, + ): Promise { + try { + if (req.method !== 'POST') { + res.writeHead(405, { Allow: 'POST' }).end( + JSON.stringify({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Method not allowed. Only POST is supported.', + }, + id: null, + }), + ); + return; + } + + const accept = req.headers['accept']; + if (!(accept && accept.includes('application/json'))) { + res.writeHead(406); + res.end( + JSON.stringify({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Not Acceptable: Client must accept application/json', + }, + id: null, + }), + ); + return; + } + + const ct = req.headers['content-type']; + if (!(ct && ct.includes('application/json'))) { + res.writeHead(415); + res.end( + JSON.stringify({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Unsupported Media Type: Content-Type must be application/json', + }, + id: null, + }), + ); + return; + } + + let raw: unknown = parsedBody; + if (raw === undefined) { + const chunks: Buffer[] = []; + await new Promise((resolve, reject) => { + req.on('data', (c) => chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c))); + req.on('end', () => resolve()); + req.on('error', reject); + }); + raw = JSON.parse(Buffer.concat(chunks).toString('utf-8')); + } + + const messages = Array.isArray(raw) + ? (raw as unknown[]).map((m) => JSONRPCMessageSchema.parse(m)) + : [JSONRPCMessageSchema.parse(raw)]; + + const hasRequests = messages.some(isJSONRPCRequest); + if (!hasRequests) { + res.writeHead(202).end(); + for (const msg of messages) { + this.onmessage?.(msg, { + requestInfo: { + headers: req.headers, + }, + authInfo: req.auth, + }); + } + return; + } + + const orderedIds: RequestId[] = messages.filter(isJSONRPCRequest).map((m) => m.id); + const connId = `${Date.now()}-${Math.random()}`; + this.connections.set(connId, { + res, + orderedIds, + pendingIds: new Set(orderedIds), + responses: new Map(), + }); + orderedIds.forEach((id) => this.requestIdToConn.set(id, connId)); + + res.on('close', () => { + this.connections.delete(connId); + orderedIds.forEach((id) => this.requestIdToConn.delete(id)); + }); + + for (const msg of messages) { + this.onmessage?.(msg, { + requestInfo: { + headers: req.headers, + }, + authInfo: req.auth, + }); + } + } catch (error) { + this.onerror?.(error as Error); + res.writeHead(400); + res.end( + JSON.stringify({ + jsonrpc: '2.0', + error: { + code: -32700, + message: 'Parse error', + data: String(error), + }, + id: null, + }), + ); + } + } +} diff --git a/src/utils/linked-api-tool.ts b/src/utils/linked-api-tool.ts index de200ff..2aeb308 100644 --- a/src/utils/linked-api-tool.ts +++ b/src/utils/linked-api-tool.ts @@ -1,5 +1,5 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import { Operation, TMappedResponse } from 'linkedapi-node'; +import LinkedApi, { Operation, TMappedResponse, TOperationName } from 'linkedapi-node'; import { LinkedApiProgressNotification } from 'src/utils/types'; import z from 'zod'; @@ -21,27 +21,24 @@ export abstract class LinkedApiTool { } public abstract execute( + linkedapi: LinkedApi, args: TParams, progressToken?: string | number, ): Promise>; } export abstract class OperationTool extends LinkedApiTool { - private readonly operation: Operation; - - constructor( - operation: Operation, - progressCallback: (progress: LinkedApiProgressNotification) => void, - ) { - super(progressCallback); - this.operation = operation; - } + public abstract readonly operationName: TOperationName; public override execute( + linkedapi: LinkedApi, args: TParams, progressToken?: string | number, ): Promise> { - return executeWithProgress(this.progressCallback, this.operation, { + const operation = linkedapi.operations.find( + (operation) => operation.operationName === this.operationName, + )! as Operation; + return executeWithProgress(this.progressCallback, operation, { params: args, progressToken, });