Skip to content

Commit 42f9d8c

Browse files
0.3.0 (#2)
1 parent 29561b6 commit 42f9d8c

40 files changed

+572
-382
lines changed

.dockerignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
node_modules
2+
npm-debug.log
3+
.DS_Store
4+
.git
5+
.gitignore
6+
dist
7+
**/.vscode
8+
**/.idea
9+
**/*.log
10+
coverage
11+
.env

.github/workflows/merge-main.yaml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: PR Merged -> main
2+
3+
on:
4+
push:
5+
branches: ['main']
6+
7+
jobs:
8+
linting:
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- name: Checkout code
13+
uses: actions/[email protected]
14+
15+
- name: Set up Node.js
16+
uses: actions/[email protected]
17+
with:
18+
node-version: 20
19+
20+
- name: Install dependencies
21+
run: npm ci
22+
23+
- name: Run ESLint
24+
run: npm run lint
25+
26+
build-push:
27+
needs: [linting]
28+
runs-on: ubuntu-latest
29+
if: always() && needs.linting.result == 'success'
30+
31+
steps:
32+
- name: Checkout code
33+
uses: actions/[email protected]
34+
35+
- name: Log in to GitHub Container Registry
36+
uses: docker/login-action@v3
37+
with:
38+
registry: ghcr.io
39+
username: ${{ github.actor }}
40+
password: ${{ secrets.GITHUB_TOKEN }}
41+
42+
- name: Set up Docker Buildx
43+
uses: docker/setup-buildx-action@v2
44+
45+
- name: Build and push
46+
uses: docker/build-push-action@v4
47+
with:
48+
context: .
49+
push: true
50+
tags: |
51+
ghcr.io/${{ github.repository_owner }}/${{ secrets.DOCKER_IMAGE_NAME }}:prod
52+
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/${{ secrets.DOCKER_IMAGE_NAME }}:prod
53+
cache-to: type=inline

Dockerfile

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
FROM node:20-alpine AS builder
2+
WORKDIR /app
3+
4+
COPY package.json package-lock.json ./
5+
RUN npm ci
6+
7+
COPY tsconfig.json ./
8+
COPY src ./src
9+
COPY README.md ./README.md
10+
RUN npm run build
11+
12+
FROM node:20-alpine AS runner
13+
ENV NODE_ENV=production
14+
WORKDIR /app
15+
16+
COPY package.json package-lock.json ./
17+
RUN npm ci --omit=dev --ignore-scripts
18+
COPY --from=builder /app/dist ./dist
19+
20+
ENV HOST=0.0.0.0 \
21+
PORT=3000
22+
23+
EXPOSE 3000
24+
25+
CMD ["node", "dist/index.js", "--http"]
26+
27+

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "linkedapi-mcp",
3-
"version": "0.2.0",
3+
"version": "0.3.0",
44
"description": "MCP server for Linked API",
55
"main": "dist/index.js",
66
"bin": {

src/index.ts

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,27 @@ import {
77
ListPromptsRequestSchema,
88
ListToolsRequestSchema,
99
} from '@modelcontextprotocol/sdk/types.js';
10+
import http from 'node:http';
1011

1112
import { LinkedApiMCPServer } from './linked-api-server';
1213
import { availablePrompts, getPromptContent, systemPrompt } from './prompts';
1314
import { debugLog } from './utils/debug-log';
15+
import { JsonHTTPServerTransport } from './utils/json-http-transport';
1416
import { LinkedApiProgressNotification } from './utils/types';
1517

16-
async function main() {
17-
const linkedApiToken = process.env.LINKED_API_TOKEN;
18-
const identificationToken = process.env.IDENTIFICATION_TOKEN;
18+
function getArgValue(flag: string): string | undefined {
19+
const index = process.argv.indexOf(flag);
20+
if (index === -1) return undefined;
21+
const value = process.argv[index + 1];
22+
if (!value || value.startsWith('--')) return undefined;
23+
return value;
24+
}
1925

26+
function hasFlag(flag: string): boolean {
27+
return process.argv.includes(flag);
28+
}
29+
30+
async function main() {
2031
const server = new Server(
2132
{
2233
name: 'linkedapi-mcp',
@@ -33,14 +44,7 @@ async function main() {
3344
);
3445

3546
const progressCallback = (_notification: LinkedApiProgressNotification) => {};
36-
37-
const linkedApiServer = new LinkedApiMCPServer(
38-
{
39-
linkedApiToken: linkedApiToken!,
40-
identificationToken: identificationToken!,
41-
},
42-
progressCallback,
43-
);
47+
const linkedApiServer = new LinkedApiMCPServer(progressCallback);
4448

4549
server.setRequestHandler(ListToolsRequestSchema, async () => {
4650
const tools = linkedApiServer.getTools();
@@ -78,15 +82,26 @@ async function main() {
7882
}
7983
});
8084

81-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
85+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
8286
debugLog('Tool request received', {
8387
toolName: request.params.name,
8488
arguments: request.params.arguments,
8589
progressToken: request.params._meta?.progressToken,
8690
});
8791

8892
try {
89-
const result = await linkedApiServer.callTool(request.params);
93+
const localLinkedApiToken = process.env.LINKED_API_TOKEN;
94+
const localIdentificationToken = process.env.IDENTIFICATION_TOKEN;
95+
const headers = extra?.requestInfo?.headers ?? {};
96+
const linkedApiToken = (headers['linked-api-token'] ?? localLinkedApiToken ?? '') as string;
97+
const identificationToken = (headers['identification-token'] ??
98+
localIdentificationToken ??
99+
'') as string;
100+
101+
const result = await linkedApiServer.executeWithTokens(request.params, {
102+
linkedApiToken,
103+
identificationToken,
104+
});
90105
return result;
91106
} catch (error) {
92107
debugLog('Tool execution failed', {
@@ -96,8 +111,52 @@ async function main() {
96111
throw error;
97112
}
98113
});
99-
const transport = new StdioServerTransport();
100-
await server.connect(transport);
114+
115+
if (hasFlag('--http') || hasFlag('--transport=http')) {
116+
const port = Number(process.env.PORT ?? getArgValue('--port') ?? 3000);
117+
const host = process.env.HOST ?? getArgValue('--host') ?? '0.0.0.0';
118+
const transport = new JsonHTTPServerTransport();
119+
120+
await server.connect(transport);
121+
122+
const httpServer = http.createServer(async (req, res) => {
123+
try {
124+
if (!req.url) {
125+
res.statusCode = 400;
126+
res.end('Bad Request');
127+
return;
128+
}
129+
const url = new URL(req.url, `http://${req.headers.host ?? 'localhost'}`);
130+
// Set query parameters to headers if they are not set
131+
const linkedApiTokenQP = url.searchParams.get('linked-api-token');
132+
const identificationTokenQP = url.searchParams.get('identification-token');
133+
if (!req.headers['linked-api-token'] && linkedApiTokenQP) {
134+
req.headers['linked-api-token'] = linkedApiTokenQP;
135+
}
136+
if (!req.headers['identification-token'] && identificationTokenQP) {
137+
req.headers['identification-token'] = identificationTokenQP;
138+
}
139+
await transport.handleRequest(req, res);
140+
} catch (error) {
141+
debugLog('HTTP request handling failed', {
142+
error: error instanceof Error ? error.message : String(error),
143+
});
144+
res.statusCode = 500;
145+
res.end('Internal Server Error');
146+
}
147+
});
148+
149+
httpServer.listen(port, host, () => {
150+
debugLog('HTTP transport listening', {
151+
host,
152+
port,
153+
});
154+
});
155+
} else {
156+
const transport = new StdioServerTransport();
157+
await server.connect(transport);
158+
debugLog('stdio transport connected');
159+
}
101160
}
102161

103162
main().catch((error) => {

src/linked-api-server.ts

Lines changed: 17 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,32 @@
11
import { Tool } from '@modelcontextprotocol/sdk/types.js';
2-
import {
3-
LinkedApi,
4-
LinkedApiError,
5-
LinkedApiWorkflowTimeoutError,
6-
TLinkedApiConfig,
7-
} from 'linkedapi-node';
2+
import { LinkedApi, LinkedApiError, TLinkedApiConfig } from 'linkedapi-node';
83
import { buildLinkedApiHttpClient } from 'linkedapi-node/dist/core';
94

105
import { LinkedApiTools } from './linked-api-tools';
116
import { debugLog } from './utils/debug-log';
7+
import { handleLinkedApiError } from './utils/handle-linked-api-error';
128
import {
139
CallToolResult,
1410
ExtendedCallToolRequest,
1511
LinkedApiProgressNotification,
1612
} from './utils/types';
1713

1814
export class LinkedApiMCPServer {
19-
private linkedapi: LinkedApi;
2015
private tools: LinkedApiTools;
21-
private progressCallback: (notification: LinkedApiProgressNotification) => void;
2216

23-
constructor(
17+
constructor(progressCallback: (notification: LinkedApiProgressNotification) => void) {
18+
this.tools = new LinkedApiTools(progressCallback);
19+
}
20+
21+
public getTools(): Tool[] {
22+
return this.tools.tools.map((tool) => tool.getTool());
23+
}
24+
25+
public async executeWithTokens(
26+
request: ExtendedCallToolRequest['params'],
2427
config: TLinkedApiConfig,
25-
progressCallback: (notification: LinkedApiProgressNotification) => void,
26-
) {
27-
this.linkedapi = new LinkedApi(
28+
): Promise<CallToolResult> {
29+
const linkedApi = new LinkedApi(
2830
buildLinkedApiHttpClient(
2931
{
3032
linkedApiToken: config.linkedApiToken!,
@@ -33,26 +35,14 @@ export class LinkedApiMCPServer {
3335
'mcp',
3436
),
3537
);
36-
this.progressCallback = progressCallback;
3738

38-
this.tools = new LinkedApiTools(this.linkedapi, this.progressCallback);
39-
}
40-
41-
public getTools(): Tool[] {
42-
return [...this.tools.tools.map((t) => t.getTool())];
43-
}
44-
45-
public async callTool(request: ExtendedCallToolRequest['params']): Promise<CallToolResult> {
4639
const { name, arguments: args, _meta } = request;
4740
const progressToken = _meta?.progressToken;
4841

4942
try {
50-
const tool = this.tools.toolByName(name);
51-
if (!tool) {
52-
throw new Error(`Unknown tool: ${name}`);
53-
}
43+
const tool = this.tools.toolByName(name)!;
5444
const params = tool.validate(args);
55-
const { data, errors } = await tool.execute(params, progressToken);
45+
const { data, errors } = await tool.execute(linkedApi, params, progressToken);
5646
if (errors.length > 0 && !data) {
5747
return {
5848
content: [
@@ -73,15 +63,7 @@ export class LinkedApiMCPServer {
7363
};
7464
} catch (error) {
7565
if (error instanceof LinkedApiError) {
76-
let body: unknown = error;
77-
if (error instanceof LinkedApiWorkflowTimeoutError) {
78-
const { message, workflowId, operationName } = error;
79-
body = {
80-
message,
81-
workflowId,
82-
operationName,
83-
};
84-
}
66+
const body = handleLinkedApiError(error);
8567
return {
8668
content: [
8769
{

src/linked-api-tools.ts

Lines changed: 27 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import LinkedApi from 'linkedapi-node';
2-
31
import { CheckConnectionStatusTool } from './tools/check-connection-status.js';
42
import { CommentOnPostTool } from './tools/comment-on-post.js';
53
import { ExecuteCustomWorkflowTool } from './tools/execute-custom-workflow.js';
@@ -32,40 +30,37 @@ import { LinkedApiProgressNotification } from './utils/types.js';
3230
export class LinkedApiTools {
3331
public readonly tools: ReadonlyArray<LinkedApiTool<unknown, unknown>>;
3432

35-
constructor(
36-
linkedapi: LinkedApi,
37-
progressCallback: (progress: LinkedApiProgressNotification) => void,
38-
) {
33+
constructor(progressCallback: (progress: LinkedApiProgressNotification) => void) {
3934
this.tools = [
4035
// Standard tools
41-
new SendMessageTool(linkedapi, progressCallback),
42-
new GetConversationTool(linkedapi, progressCallback),
43-
new CheckConnectionStatusTool(linkedapi, progressCallback),
44-
new RetrieveConnectionsTool(linkedapi, progressCallback),
45-
new SendConnectionRequestTool(linkedapi, progressCallback),
46-
new WithdrawConnectionRequestTool(linkedapi, progressCallback),
47-
new RetrievePendingRequestsTool(linkedapi, progressCallback),
48-
new RemoveConnectionTool(linkedapi, progressCallback),
49-
new SearchCompaniesTool(linkedapi, progressCallback),
50-
new SearchPeopleTool(linkedapi, progressCallback),
51-
new FetchCompanyTool(linkedapi, progressCallback),
52-
new FetchPersonTool(linkedapi, progressCallback),
53-
new FetchPostTool(linkedapi, progressCallback),
54-
new ReactToPostTool(linkedapi, progressCallback),
55-
new CommentOnPostTool(linkedapi, progressCallback),
56-
new RetrieveSSITool(linkedapi, progressCallback),
57-
new RetrievePerformanceTool(linkedapi, progressCallback),
36+
new SendMessageTool(progressCallback),
37+
new GetConversationTool(progressCallback),
38+
new CheckConnectionStatusTool(progressCallback),
39+
new RetrieveConnectionsTool(progressCallback),
40+
new SendConnectionRequestTool(progressCallback),
41+
new WithdrawConnectionRequestTool(progressCallback),
42+
new RetrievePendingRequestsTool(progressCallback),
43+
new RemoveConnectionTool(progressCallback),
44+
new SearchCompaniesTool(progressCallback),
45+
new SearchPeopleTool(progressCallback),
46+
new FetchCompanyTool(progressCallback),
47+
new FetchPersonTool(progressCallback),
48+
new FetchPostTool(progressCallback),
49+
new ReactToPostTool(progressCallback),
50+
new CommentOnPostTool(progressCallback),
51+
new RetrieveSSITool(progressCallback),
52+
new RetrievePerformanceTool(progressCallback),
5853
// Sales Navigator tools
59-
new NvSendMessageTool(linkedapi, progressCallback),
60-
new NvGetConversationTool(linkedapi, progressCallback),
61-
new NvSearchCompaniesTool(linkedapi, progressCallback),
62-
new NvSearchPeopleTool(linkedapi, progressCallback),
63-
new NvFetchCompanyTool(linkedapi, progressCallback),
64-
new NvFetchPersonTool(linkedapi, progressCallback),
54+
new NvSendMessageTool(progressCallback),
55+
new NvGetConversationTool(progressCallback),
56+
new NvSearchCompaniesTool(progressCallback),
57+
new NvSearchPeopleTool(progressCallback),
58+
new NvFetchCompanyTool(progressCallback),
59+
new NvFetchPersonTool(progressCallback),
6560
// Other tools
66-
new ExecuteCustomWorkflowTool(linkedapi, progressCallback),
67-
new GetWorkflowResultTool(linkedapi, progressCallback),
68-
new GetApiUsageTool(linkedapi, progressCallback),
61+
new ExecuteCustomWorkflowTool(progressCallback),
62+
new GetWorkflowResultTool(progressCallback),
63+
new GetApiUsageTool(progressCallback),
6964
];
7065
}
7166

0 commit comments

Comments
 (0)