Skip to content

Commit c47aae7

Browse files
Description improvements
1 parent 6079941 commit c47aae7

22 files changed

Lines changed: 333 additions & 120 deletions

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
11
Linked API MCP server connects your LinkedIn account to AI assistants like Claude, Cursor, and VS Code. Ask them to search for leads, send messages, analyze profiles, and much more – they'll handle it through our cloud browser, safely and automatically.
22

33
## Use cases
4+
45
- **Sales automation assistant**. Ask your AI to find leads, check their profiles, and draft personalized outreach. It can search for "software engineers at companies with 50-200 employees in San Francisco", analyze their backgrounds, and suggest connection messages that actually make sense.
56
- **Recruitment assistant**. Let your assistant search for candidates with specific skills, review their experience, and send initial outreach. It handles the time-consuming parts while you focus on actually talking to people.
67
- **Conversation assistant**. Your AI can read your existing LinkedIn conversations and help you respond naturally. It understands the context of your chats, suggests relevant replies, and can even send follow-up messages.
78
- **Market research assistant**. Need competitor analysis? Your assistant can gather data about companies, their employees, and recent activities. Get insights about industry trends without spending hours on LinkedIn.
89

910
## Get started
11+
1012
To start using Linked API MCP, spend 2 minutes reading these essential guides:
1113

1214
1. [Installation](https://linkedapi.io/mcp/installation/) – set up MCP in Claude, Cursor, VS Code, or Windsurf.
1315
2. [Available tools](https://linkedapi.io/mcp/available-tools/) – explore all the LinkedIn tools your assistant can call.
1416
3. [Usage examples](https://linkedapi.io/mcp/usage-examples/) – see real-world examples to get you started quickly.
1517

18+
## Long-running actions
19+
20+
Linked API actions run through a cloud browser and are queued like normal automation. Many actions take several minutes, especially searches and profile fetches with optional data.
21+
22+
If a tool returns `workflowId` and `operationName`, the action is still running. Do not retry the original tool because that can queue duplicate work. Call `get_workflow_result` with the exact `workflowId` and `operationName` until the final result is returned.
23+
1624
## License
25+
1726
This project is licensed under the MIT – see the [LICENSE](https://github.com/Linked-API/linkedapi-mcp/blob/main/LICENSE) file for details.

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": "1.0.0",
3+
"version": "1.0.1",
44
"description": "MCP server that lets AI assistants control LinkedIn accounts and retrieve real-time data.",
55
"main": "dist/index.js",
66
"bin": {

src/index.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import { LinkedApiProgressNotification } from './utils/types';
1717

1818
function deriveClientFromUserAgent(userAgent: string): string {
1919
const ua = userAgent.toLowerCase();
20+
if (ua.includes('codex')) return 'codex';
21+
if (ua.includes('claude-code') || ua.includes('claude code')) return 'claude-code';
22+
if (ua.includes('claude') || ua.includes('anthropic')) return 'claude';
2023
if (ua.includes('cursor')) return 'cursor';
2124
if (ua.includes('windsurf')) return 'windsurf';
2225
if (ua.includes('vscode') || ua.includes('visual studio code')) return 'vscode';
@@ -33,6 +36,14 @@ function deriveClientFromUserAgent(userAgent: string): string {
3336
return userAgent;
3437
}
3538

39+
function normalizeHeaderValue(value: string | Array<string> | undefined): string | undefined {
40+
if (Array.isArray(value)) {
41+
return value.find((item) => item.trim().length > 0)?.trim();
42+
}
43+
const normalizedValue = value?.trim();
44+
return normalizedValue && normalizedValue.length > 0 ? normalizedValue : undefined;
45+
}
46+
3647
function getArgValue(flag: string): string | undefined {
3748
const index = process.argv.indexOf(flag);
3849
if (index === -1) return undefined;
@@ -61,8 +72,7 @@ async function main() {
6172
},
6273
);
6374

64-
const progressCallback = (_notification: LinkedApiProgressNotification) => {};
65-
const linkedApiServer = new LinkedApiMCPServer(progressCallback);
75+
const linkedApiServer = new LinkedApiMCPServer();
6676

6777
server.setRequestHandler(ListToolsRequestSchema, async () => {
6878
const tools = linkedApiServer.getTools();
@@ -118,22 +128,49 @@ async function main() {
118128
const localLinkedApiToken = process.env.LINKED_API_TOKEN;
119129
const localIdentificationToken = process.env.IDENTIFICATION_TOKEN;
120130
const headers = extra?.requestInfo?.headers ?? {};
121-
const linkedApiToken = (headers['linked-api-token'] ?? localLinkedApiToken ?? '') as string;
122-
const identificationToken = (headers['identification-token'] ??
123-
localIdentificationToken ??
124-
'') as string;
125-
let mcpClient = (headers['client'] ?? '') as string;
131+
const linkedApiToken =
132+
normalizeHeaderValue(headers['linked-api-token']) ?? localLinkedApiToken ?? '';
133+
const identificationToken =
134+
normalizeHeaderValue(headers['identification-token']) ?? localIdentificationToken ?? '';
135+
let mcpClient = normalizeHeaderValue(headers['client']) ?? '';
126136
if (!mcpClient) {
127-
const userAgentHeader = headers['user-agent'];
128-
if (typeof userAgentHeader === 'string' && userAgentHeader.trim().length > 0) {
137+
const userAgentHeader = normalizeHeaderValue(headers['user-agent']);
138+
if (userAgentHeader) {
129139
mcpClient = deriveClientFromUserAgent(userAgentHeader);
130140
}
131141
}
142+
const progressCallback = (notification: LinkedApiProgressNotification): void => {
143+
const { progressToken, progress, total, message } = notification;
144+
if (progressToken === undefined) {
145+
return;
146+
}
147+
148+
void extra
149+
.sendNotification({
150+
method: 'notifications/progress',
151+
params: {
152+
progressToken,
153+
progress,
154+
total,
155+
message,
156+
},
157+
})
158+
.catch((error: unknown) => {
159+
logger.warn(
160+
{
161+
toolName: request.params.name,
162+
error: error instanceof Error ? error.message : String(error),
163+
},
164+
'Failed to send MCP progress notification',
165+
);
166+
});
167+
};
132168

133169
const result = await linkedApiServer.executeWithTokens(request.params, {
134170
linkedApiToken,
135171
identificationToken,
136172
mcpClient,
173+
progressCallback,
137174
});
138175
return result;
139176
} catch (error) {

src/linked-api-server.ts

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,47 @@ import {
1212
LinkedApiProgressNotification,
1313
} from './utils/types';
1414

15+
const BACKGROUND_WORKFLOW_DESCRIPTION =
16+
`Linked API actions are queued into a cloud-browser workflow and may take several minutes. This is normal LinkedIn automation behavior, not a failed request. If the response contains workflowId and operationName, do not retry this tool; call get_workflow_result with those exact values until the final result is returned.` as const;
17+
const NON_WORKFLOW_TOOL_NAMES = new Set<string>(['get_workflow_result', 'get_api_usage'] as const);
18+
const NOOP_PROGRESS_CALLBACK = (_notification: LinkedApiProgressNotification): void => {};
19+
20+
interface TExecuteWithTokensOptions extends TLinkedApiConfig {
21+
mcpClient: string;
22+
progressCallback?: (notification: LinkedApiProgressNotification) => void;
23+
}
24+
1525
export class LinkedApiMCPServer {
1626
private tools: LinkedApiTools;
1727

18-
constructor(progressCallback: (notification: LinkedApiProgressNotification) => void) {
19-
this.tools = new LinkedApiTools(progressCallback);
28+
constructor() {
29+
this.tools = new LinkedApiTools();
2030
}
2131

2232
public getTools(): Tool[] {
23-
const linkedApiTools = this.tools.tools.map((tool) => tool.getTool());
33+
const linkedApiTools = this.tools.tools.map((tool) => {
34+
const definition = tool.getTool();
35+
if (NON_WORKFLOW_TOOL_NAMES.has(definition.name)) {
36+
return definition;
37+
}
38+
39+
return {
40+
...definition,
41+
description: `${definition.description}\n\n${BACKGROUND_WORKFLOW_DESCRIPTION}`,
42+
};
43+
});
2444
const adminTools = this.tools.adminTools.map((tool) => tool.getTool());
2545
return [...linkedApiTools, ...adminTools];
2646
}
2747

2848
public async executeWithTokens(
2949
request: ExtendedCallToolRequest['params'],
30-
{ linkedApiToken, identificationToken, mcpClient }: TLinkedApiConfig & { mcpClient: string },
50+
{
51+
linkedApiToken,
52+
identificationToken,
53+
mcpClient,
54+
progressCallback = NOOP_PROGRESS_CALLBACK,
55+
}: TExecuteWithTokensOptions,
3156
): Promise<CallToolResult> {
3257
const workflowTimeout = defineRequestTimeoutInSeconds(mcpClient) * 1000;
3358
logger.info(
@@ -46,22 +71,41 @@ export class LinkedApiMCPServer {
4671
try {
4772
const adminTool = this.tools.adminToolByName(toolName);
4873
if (adminTool) {
49-
const admin = new LinkedApiAdmin({ linkedApiToken,
50-
client: 'mcp' });
74+
const admin = new LinkedApiAdmin({
75+
linkedApiToken,
76+
client: 'mcp',
77+
});
5178
const params = adminTool.validate(args);
52-
const result = await adminTool.execute({ admin,
53-
args: params });
79+
const result = await adminTool.execute({
80+
admin,
81+
args: params,
82+
});
5483
const duration = this.calculateDuration(startTime);
55-
logger.info({ toolName,
56-
duration,
57-
data: result }, 'Tool execution successful');
84+
logger.info(
85+
{
86+
toolName,
87+
duration,
88+
data: result,
89+
},
90+
'Tool execution successful',
91+
);
5892
if (result === undefined) {
59-
return { content: [{ type: 'text' as const,
60-
text: 'Completed' }] };
93+
return {
94+
content: [
95+
{
96+
type: 'text' as const,
97+
text: 'Completed',
98+
},
99+
],
100+
};
61101
}
62102
return {
63-
content: [{ type: 'text' as const,
64-
text: JSON.stringify(result, null, 2) }],
103+
content: [
104+
{
105+
type: 'text' as const,
106+
text: JSON.stringify(result, null, 2),
107+
},
108+
],
65109
};
66110
}
67111

@@ -82,9 +126,10 @@ text: JSON.stringify(result, null, 2) }],
82126
const params = tool.validate(args);
83127
const { data, errors } = await tool.execute({
84128
linkedapi,
85-
args: params,
129+
args: params as never,
86130
workflowTimeout,
87131
progressToken,
132+
progressCallback,
88133
});
89134
const endTime = Date.now();
90135
const duration = `${((endTime - startTime) / 1000).toFixed(2)} seconds`;
@@ -98,6 +143,7 @@ text: JSON.stringify(result, null, 2) }],
98143
'Tool execution failed',
99144
);
100145
return {
146+
isError: true,
101147
content: [
102148
{
103149
type: 'text' as const,
@@ -151,6 +197,8 @@ text: JSON.stringify(result, null, 2) }],
151197
text: JSON.stringify(body, null, 2),
152198
},
153199
],
200+
structuredContent: body,
201+
isError: error.type !== 'workflowTimeout',
154202
};
155203
}
156204
const errorMessage = error instanceof Error ? error.message : String(error);

src/linked-api-tools.ts

Lines changed: 52 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import LinkedApi, { TMappedResponse } from '@linkedapi/node';
2+
import { Tool } from '@modelcontextprotocol/sdk/types.js';
3+
14
import { AdminConnectAccountTool } from './tools/admin-connect-account.js';
25
import { AdminDisconnectAccountTool } from './tools/admin-disconnect-account.js';
36
import { AdminGetAccountsTool } from './tools/admin-get-accounts.js';
@@ -36,45 +39,63 @@ import { SendConnectionRequestTool } from './tools/send-connection-request.js';
3639
import { SendMessageTool } from './tools/send-message.js';
3740
import { WithdrawConnectionRequestTool } from './tools/withdraw-connection-request.js';
3841
import { AdminTool } from './utils/admin-tool.js';
39-
import { LinkedApiTool } from './utils/linked-api-tool.js';
4042
import { LinkedApiProgressNotification } from './utils/types.js';
4143

44+
interface TRegisteredLinkedApiTool {
45+
readonly name: string;
46+
getTool(): Tool;
47+
validate(args: unknown): unknown;
48+
execute({
49+
linkedapi,
50+
args,
51+
workflowTimeout,
52+
progressToken,
53+
progressCallback,
54+
}: {
55+
linkedapi: LinkedApi;
56+
args: never;
57+
workflowTimeout: number;
58+
progressToken?: string | number;
59+
progressCallback: (progress: LinkedApiProgressNotification) => void;
60+
}): Promise<TMappedResponse<unknown>>;
61+
}
62+
4263
export class LinkedApiTools {
43-
public readonly tools: ReadonlyArray<LinkedApiTool<unknown, unknown>>;
64+
public readonly tools: ReadonlyArray<TRegisteredLinkedApiTool>;
4465
public readonly adminTools: ReadonlyArray<AdminTool<unknown, unknown>>;
4566

46-
constructor(progressCallback: (progress: LinkedApiProgressNotification) => void) {
67+
constructor() {
4768
this.tools = [
4869
// Standard tools
49-
new SendMessageTool(progressCallback),
50-
new GetConversationTool(progressCallback),
51-
new CheckConnectionStatusTool(progressCallback),
52-
new RetrieveConnectionsTool(progressCallback),
53-
new SendConnectionRequestTool(progressCallback),
54-
new WithdrawConnectionRequestTool(progressCallback),
55-
new RetrievePendingRequestsTool(progressCallback),
56-
new RemoveConnectionTool(progressCallback),
57-
new SearchCompaniesTool(progressCallback),
58-
new SearchPeopleTool(progressCallback),
59-
new FetchCompanyTool(progressCallback),
60-
new FetchPersonTool(progressCallback),
61-
new FetchPostTool(progressCallback),
62-
new ReactToPostTool(progressCallback),
63-
new CommentOnPostTool(progressCallback),
64-
new CreatePostTool(progressCallback),
65-
new RetrieveSSITool(progressCallback),
66-
new RetrievePerformanceTool(progressCallback),
70+
new SendMessageTool(),
71+
new GetConversationTool(),
72+
new CheckConnectionStatusTool(),
73+
new RetrieveConnectionsTool(),
74+
new SendConnectionRequestTool(),
75+
new WithdrawConnectionRequestTool(),
76+
new RetrievePendingRequestsTool(),
77+
new RemoveConnectionTool(),
78+
new SearchCompaniesTool(),
79+
new SearchPeopleTool(),
80+
new FetchCompanyTool(),
81+
new FetchPersonTool(),
82+
new FetchPostTool(),
83+
new ReactToPostTool(),
84+
new CommentOnPostTool(),
85+
new CreatePostTool(),
86+
new RetrieveSSITool(),
87+
new RetrievePerformanceTool(),
6788
// Sales Navigator tools
68-
new NvSendMessageTool(progressCallback),
69-
new NvGetConversationTool(progressCallback),
70-
new NvSearchCompaniesTool(progressCallback),
71-
new NvSearchPeopleTool(progressCallback),
72-
new NvFetchCompanyTool(progressCallback),
73-
new NvFetchPersonTool(progressCallback),
89+
new NvSendMessageTool(),
90+
new NvGetConversationTool(),
91+
new NvSearchCompaniesTool(),
92+
new NvSearchPeopleTool(),
93+
new NvFetchCompanyTool(),
94+
new NvFetchPersonTool(),
7495
// Other tools
75-
new ExecuteCustomWorkflowTool(progressCallback),
76-
new GetWorkflowResultTool(progressCallback),
77-
new GetApiUsageTool(progressCallback),
96+
new ExecuteCustomWorkflowTool(),
97+
new GetWorkflowResultTool(),
98+
new GetApiUsageTool(),
7899
];
79100

80101
this.adminTools = [
@@ -91,7 +112,7 @@ export class LinkedApiTools {
91112
];
92113
}
93114

94-
public toolByName(name: string): LinkedApiTool<unknown, unknown> | undefined {
115+
public toolByName(name: string): TRegisteredLinkedApiTool | undefined {
95116
return this.tools.find((tool) => tool.name === name);
96117
}
97118

src/tools/comment-on-post.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ export class CommentOnPostTool extends OperationTool<TCommentOnPostParams, unkno
1616
public override getTool(): Tool {
1717
return {
1818
name: this.name,
19-
description: 'Allows you to leave a comment on a post (st.commentOnPost action).',
19+
description:
20+
'Allows you to leave a comment on a post (st.commentOnPost action). If this workflow is still running, do not retry this tool; retrying can post duplicate comments.',
2021
inputSchema: {
2122
type: 'object',
2223
properties: {

src/tools/create-post.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class CreatePostTool extends OperationTool<TCreatePostParams, unknown> {
2626
return {
2727
name: this.name,
2828
description:
29-
'Creates a new LinkedIn post with optional media attachments (st.createPost action).',
29+
'Creates a new LinkedIn post with optional media attachments (st.createPost action). If this workflow is still running, do not retry this tool; retrying can create duplicate posts.',
3030
inputSchema: {
3131
type: 'object',
3232
properties: {

src/tools/execute-custom-workflow.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ export class ExecuteCustomWorkflowTool extends OperationTool<
1717
public override getTool(): Tool {
1818
return {
1919
name: this.name,
20-
description: 'Execute a custom workflow definition',
20+
description:
21+
'Execute a custom workflow definition. If this workflow is still running, do not retry this tool; retrying can duplicate any write actions inside the custom workflow.',
2122
inputSchema: {
2223
type: 'object',
2324
properties: { definition: { type: 'object' } },

0 commit comments

Comments
 (0)