From 1e500a9a151ecf2c427edaa971b20137c42d9ea4 Mon Sep 17 00:00:00 2001 From: Maksim Redzkin Date: Tue, 29 Sep 2026 12:09:45 +0300 Subject: [PATCH] add mentions & st.createRepost action --- package-lock.json | 12 ++--- package.json | 6 +-- src/linked-api-tools.ts | 2 + src/tools/comment-on-post.ts | 12 +++-- src/tools/create-post.ts | 51 +++++++++++++++++++- src/tools/create-repost.ts | 90 ++++++++++++++++++++++++++++++++++++ src/tools/fetch-post.ts | 12 +++-- src/tools/react-to-post.ts | 12 +++-- 8 files changed, 177 insertions(+), 20 deletions(-) create mode 100644 src/tools/create-repost.ts diff --git a/package-lock.json b/package-lock.json index 649142a..bddef0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@linkedapi/mcp", - "version": "2.3.12", + "version": "2.3.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@linkedapi/mcp", - "version": "2.3.12", + "version": "2.3.13", "license": "MIT", "dependencies": { - "@linkedapi/node": "^2.3.12", + "@linkedapi/node": "^2.3.13", "@modelcontextprotocol/sdk": "^1.17.4", "zod": "^4.1.1" }, @@ -921,9 +921,9 @@ } }, "node_modules/@linkedapi/node": { - "version": "2.3.12", - "resolved": "https://registry.npmjs.org/@linkedapi/node/-/node-2.3.12.tgz", - "integrity": "sha512-E22zICF/fe/+riwRlnZnxWW76uqZA+/PHmmBFIqHKFXYpDUd0szaYF7SdQ503FRqQkGwBxOFJJhLxdLqvP5JCg==", + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@linkedapi/node/-/node-2.3.13.tgz", + "integrity": "sha512-DitmtWHepG/S682fk4mHpatsmCq/b3nwwM4iP1D/vOzbW9W3yiET+2hza+yWb6si6jeVkpAGFW5hg98mgGN88w==", "license": "MIT" }, "node_modules/@modelcontextprotocol/sdk": { diff --git a/package.json b/package.json index 8be191c..6cddf47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@linkedapi/mcp", - "version": "2.3.12", + "version": "2.3.13", "description": "MCP server that lets AI assistants control LinkedIn accounts and retrieve real-time data.", "main": "dist/index.js", "bin": { @@ -30,7 +30,7 @@ "author": "Linked API", "license": "MIT", "dependencies": { - "@linkedapi/node": "^2.3.12", + "@linkedapi/node": "^2.3.13", "@modelcontextprotocol/sdk": "^1.17.4", "zod": "^4.1.1" }, @@ -56,4 +56,4 @@ "typescript": "^5.8.3", "typescript-eslint": "^8.38.0" } -} +} \ No newline at end of file diff --git a/src/linked-api-tools.ts b/src/linked-api-tools.ts index f9581d1..1832950 100644 --- a/src/linked-api-tools.ts +++ b/src/linked-api-tools.ts @@ -22,6 +22,7 @@ import { AdminSetSeatsTool } from './tools/admin-set-seats.js'; import { CheckConnectionStatusTool } from './tools/check-connection-status.js'; import { CommentOnPostTool } from './tools/comment-on-post.js'; import { CreatePostTool } from './tools/create-post.js'; +import { CreateRepostTool } from './tools/create-repost.js'; import { ExecuteCustomWorkflowTool } from './tools/execute-custom-workflow.js'; import { FetchCompanyTool } from './tools/fetch-company.js'; import { FetchJobTool } from './tools/fetch-job.js'; @@ -114,6 +115,7 @@ export class LinkedApiTools { new ReactToCommentTool(), new ReplyToCommentTool(), new CreatePostTool(), + new CreateRepostTool(), new RetrieveFeedTool(), new RetrieveProfileViewersTool(), new RetrieveSSITool(), diff --git a/src/tools/comment-on-post.ts b/src/tools/comment-on-post.ts index adf0577..0ceafb6 100644 --- a/src/tools/comment-on-post.ts +++ b/src/tools/comment-on-post.ts @@ -8,7 +8,8 @@ export class CommentOnPostTool extends OperationTool { public override readonly operationName = OPERATION_NAME.createPost; protected override readonly schema = z.object({ text: z.string().min(1).max(3000), + mentions: z + .array( + z.object({ + key: z.string(), + name: z.string(), + urn: z.string().optional(), + personHashedUrl: z.string().optional(), + companyHashedUrl: z.string().optional(), + }), + ) + .optional(), companyUrl: z.string().optional(), attachments: z .array( @@ -26,13 +37,49 @@ export class CreatePostTool extends OperationTool { return { name: this.name, description: - '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.', + 'Creates a new LinkedIn post with optional mentions and media attachments (st.createPost action). Returns the URL and URN of the new post. If this workflow is still running, do not retry this tool; retrying can create duplicate posts.', inputSchema: { type: 'object', properties: { text: { type: 'string', - description: 'Post content, must be up to 3000 characters.', + description: + 'Post content, must be up to 3000 characters. Write "@[key]" where a mention should appear and describe that key in mentions.', + }, + mentions: { + type: 'array', + description: + 'People and companies to mention in the text. Each item binds a "@[key]" placeholder to one entity.', + items: { + type: 'object', + properties: { + key: { + type: 'string', + description: + 'Placeholder name used in the text as "@[key]". 1-30 characters: letters, digits, underscore or hyphen.', + }, + name: { + type: 'string', + description: + 'Name to find the person or company by. Always required: LinkedIn resolves a mention through its own name suggestions.', + }, + urn: { + type: 'string', + description: + "URN of the entity (e.g., 'urn:li:member:123456789' or 'urn:li:organization:1234567'). Decides which of the offered namesakes is taken.", + }, + personHashedUrl: { + type: 'string', + description: + 'Hashed LinkedIn profile URL of the person, as an alternative to urn.', + }, + companyHashedUrl: { + type: 'string', + description: 'Hashed LinkedIn company page URL, as an alternative to urn.', + }, + }, + required: ['key', 'name'], + }, }, companyUrl: { type: 'string', diff --git a/src/tools/create-repost.ts b/src/tools/create-repost.ts new file mode 100644 index 0000000..203f375 --- /dev/null +++ b/src/tools/create-repost.ts @@ -0,0 +1,90 @@ +import { OPERATION_NAME, TCreateRepostParams } from '@linkedapi/node'; +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { z } from 'zod'; + +import { OperationTool } from '../utils/linked-api-tool.js'; + +export class CreateRepostTool extends OperationTool { + public override readonly name = 'create_repost'; + public override readonly operationName = OPERATION_NAME.createRepost; + protected override readonly schema = z.object({ + postUrl: z.string().optional(), + postUrn: z.string().optional(), + text: z.string().max(3000).optional(), + mentions: z + .array( + z.object({ + key: z.string(), + name: z.string(), + urn: z.string().optional(), + personHashedUrl: z.string().optional(), + companyHashedUrl: z.string().optional(), + }), + ) + .optional(), + }); + + public override getTool(): Tool { + return { + name: this.name, + description: + 'Reposts a LinkedIn post, either as is or with your own commentary (st.createRepost action). Returns the URL and URN of the repost itself, which is a post of your own account, not of the post that was reshared. Reposting the same post twice from the same account fails with alreadyReposted. If this workflow is still running, do not retry this tool; retrying can create duplicate reposts.', + inputSchema: { + type: 'object', + properties: { + postUrl: { + type: 'string', + description: + "LinkedIn URL of the post to repost. (e.g., 'https://www.linkedin.com/posts/username_activity-id') Provide this or postUrn.", + }, + postUrn: { + type: 'string', + description: + "URN of the post to repost, as an alternative to postUrl. (e.g., 'urn:li:activity:1234567890123456789')", + }, + text: { + type: 'string', + description: + 'Your own commentary, up to 3000 characters. Without it the post is reposted as is. Write "@[key]" where a mention should appear and describe that key in mentions.', + }, + mentions: { + type: 'array', + description: + 'People and companies to mention in the commentary. Each item binds a "@[key]" placeholder to one entity.', + items: { + type: 'object', + properties: { + key: { + type: 'string', + description: + 'Placeholder name used in the text as "@[key]". 1-30 characters: letters, digits, underscore or hyphen.', + }, + name: { + type: 'string', + description: + 'Name to find the person or company by. Always required: LinkedIn resolves a mention through its own name suggestions.', + }, + urn: { + type: 'string', + description: + "URN of the entity (e.g., 'urn:li:member:123456789' or 'urn:li:organization:1234567'). Decides which of the offered namesakes is taken.", + }, + personHashedUrl: { + type: 'string', + description: + 'Hashed LinkedIn profile URL of the person, as an alternative to urn.', + }, + companyHashedUrl: { + type: 'string', + description: 'Hashed LinkedIn company page URL, as an alternative to urn.', + }, + }, + required: ['key', 'name'], + }, + }, + }, + required: [], + }, + }; + } +} diff --git a/src/tools/fetch-post.ts b/src/tools/fetch-post.ts index aec1566..6c1e8ef 100644 --- a/src/tools/fetch-post.ts +++ b/src/tools/fetch-post.ts @@ -8,7 +8,8 @@ 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(), + postUrl: z.string().optional(), + postUrn: z.string().optional(), retrieveComments: z.boolean().optional(), retrieveReactions: z.boolean().optional(), commentsRetrievalConfig: z @@ -36,7 +37,12 @@ export class FetchPostTool extends OperationTool { postUrl: { type: 'string', description: - "LinkedIn URL of the post. (e.g., 'https://www.linkedin.com/posts/username_activity-id')", + "LinkedIn URL of the post. (e.g., 'https://www.linkedin.com/posts/username_activity-id') Provide this or postUrn.", + }, + postUrn: { + type: 'string', + description: + "URN of the post to fetch, as an alternative to postUrl. (e.g., 'urn:li:activity:1234567890123456789')", }, retrieveComments: { type: 'boolean', @@ -83,7 +89,7 @@ export class FetchPostTool extends OperationTool { }, }, }, - required: ['postUrl'], + required: [], }, }; } diff --git a/src/tools/react-to-post.ts b/src/tools/react-to-post.ts index 0bb0c1e..9f18ba6 100644 --- a/src/tools/react-to-post.ts +++ b/src/tools/react-to-post.ts @@ -8,7 +8,8 @@ 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(), + postUrl: z.string().optional(), + postUrn: z.string().optional(), type: z.enum(['like', 'love', 'celebrate', 'support', 'funny', 'insightful']).or(z.string()), companyUrl: z.string().optional(), }); @@ -24,7 +25,12 @@ export class ReactToPostTool extends OperationTool postUrl: { type: 'string', description: - "LinkedIn URL of the post to react. (e.g., 'https://www.linkedin.com/posts/username_activity-id')", + "LinkedIn URL of the post to react. (e.g., 'https://www.linkedin.com/posts/username_activity-id') Provide this or postUrn.", + }, + postUrn: { + type: 'string', + description: + "URN of the post to react to, as an alternative to postUrl. (e.g., 'urn:li:activity:1234567890123456789')", }, type: { type: 'string', @@ -37,7 +43,7 @@ export class ReactToPostTool extends OperationTool "LinkedIn company page URL. If specified, the reaction will be added on behalf of the company. (e.g., 'https://www.linkedin.com/company/acme-corp')", }, }, - required: ['postUrl', 'type'], + required: ['type'], }, }; }