{"openapi":"3.1.0","info":{"title":"2pr.io MCP API","version":"1.0.0","summary":"Draft, schedule and publish LinkedIn posts, manage replies, search viral posts and read analytics through a 2pr.io account.","description":"The 2pr.io agent surface is a Model Context Protocol server (MCP 2025-06-18, Streamable HTTP) at `/api/mcp`.\n\nIt exposes 43 tools; each one is described under `components.schemas` as a ready-to-send `tools/call` request.\n\nAccess requires a 2pr.io account on the PRO or Team plan, authenticated with OAuth 2.1 (PKCE, dynamic client registration) or a personal bearer token created in the app.\n\nIt is not a LinkedIn scraping API and only acts on accounts connected to 2pr.io through LinkedIn's official OAuth.","termsOfService":"https://2pr.io/terms","contact":{"name":"2pr.io","email":"hi@2pr.io","url":"https://2pr.io/contact"}},"externalDocs":{"description":"2pr.io MCP server documentation","url":"https://2pr.io/docs/mcp"},"servers":[{"url":"https://2pr.io"}],"security":[{"oauth2":["mcp"]},{"personalToken":[]}],"tags":[{"name":"mcp","description":"The MCP endpoint. JSON-RPC 2.0 over POST."},{"name":"oauth","description":"OAuth 2.1 authorization server used by \"Sign in with 2pr\"."},{"name":"discovery","description":"Unauthenticated metadata documents."}],"paths":{"/api/mcp":{"post":{"tags":["mcp"],"operationId":"mcpRequest","summary":"Send a JSON-RPC request to the MCP server","description":"Supported methods: `initialize`, `ping`, `tools/list`, `tools/call`. Notifications (no `id`) get an empty 202. Tool errors from the product (validation, LinkedIn, entitlement) come back as a 200 with `result.isError: true`; transport-level failures use JSON-RPC `error`. Requests are rate limited per token, with a tighter limit on tools that call an LLM or fetch LinkedIn data.","parameters":[{"name":"MCP-Protocol-Version","in":"header","required":false,"schema":{"type":"string","example":"2025-06-18"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/InitializeRequest"},{"$ref":"#/components/schemas/PingRequest"},{"$ref":"#/components/schemas/ToolsListRequest"},{"$ref":"#/components/schemas/ToolsCallRequest"}],"discriminator":{"propertyName":"method","mapping":{"initialize":"#/components/schemas/InitializeRequest","ping":"#/components/schemas/PingRequest","tools/list":"#/components/schemas/ToolsListRequest","tools/call":"#/components/schemas/ToolsCallRequest"}}}}}},"responses":{"200":{"description":"JSON-RPC response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JsonRpcResponse"}}}},"202":{"description":"Notification accepted. Empty body."},"401":{"description":"Missing, malformed, expired or revoked token. Carries `WWW-Authenticate: Bearer resource_metadata=\"…\"` pointing at the protected-resource metadata (RFC 9728).","headers":{"WWW-Authenticate":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The account is not on a PRO or Team plan. Checked on every request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/oauth/register":{"post":{"tags":["oauth"],"operationId":"oauthRegisterClient","summary":"Register a public OAuth client (RFC 7591)","description":"No authentication. Rate limited by IP.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["redirect_uris"],"properties":{"client_name":{"type":"string"},"redirect_uris":{"type":"array","items":{"type":"string","format":"uri"},"minItems":1}}}}}},"responses":{"201":{"description":"Client registered.","content":{"application/json":{"schema":{"type":"object","properties":{"client_id":{"type":"string"},"client_name":{"type":"string"},"redirect_uris":{"type":"array","items":{"type":"string","format":"uri"}},"token_endpoint_auth_method":{"type":"string","const":"none"},"grant_types":{"type":"array","items":{"type":"string"}},"response_types":{"type":"array","items":{"type":"string"}}}}}}},"400":{"description":"Invalid registration request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/api/oauth/token":{"post":{"tags":["oauth"],"operationId":"oauthToken","summary":"Exchange an authorization code or refresh token (OAuth 2.1)","description":"PKCE S256 is required for `authorization_code`. Public clients only (`token_endpoint_auth_method: none`). Refresh tokens rotate on use.","security":[],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/TokenRequest"}},"application/json":{"schema":{"$ref":"#/components/schemas/TokenRequest"}}}},"responses":{"200":{"description":"Tokens issued.","content":{"application/json":{"schema":{"type":"object","properties":{"access_token":{"type":"string"},"token_type":{"type":"string","const":"Bearer"},"expires_in":{"type":"integer"},"refresh_token":{"type":"string"},"scope":{"type":"string","const":"mcp"}}}}}},"400":{"description":"Invalid grant, client, code verifier or redirect URI.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/.well-known/oauth-protected-resource":{"get":{"tags":["discovery"],"operationId":"protectedResourceMetadata","summary":"Protected resource metadata (RFC 9728)","security":[],"responses":{"200":{"description":"JSON metadata."}}}},"/.well-known/oauth-authorization-server":{"get":{"tags":["discovery"],"operationId":"authorizationServerMetadata","summary":"Authorization server metadata (RFC 8414)","security":[],"responses":{"200":{"description":"JSON metadata."}}}},"/.well-known/mcp.json":{"get":{"tags":["discovery"],"operationId":"mcpManifest","summary":"MCP discovery manifest","security":[],"responses":{"200":{"description":"JSON manifest."}}}}},"components":{"securitySchemes":{"oauth2":{"type":"oauth2","description":"\"Sign in with 2pr\". Authorization code with PKCE S256; register a client first.","flows":{"authorizationCode":{"authorizationUrl":"https://2pr.io/oauth/authorize","tokenUrl":"https://2pr.io/api/oauth/token","refreshUrl":"https://2pr.io/api/oauth/token","scopes":{"mcp":"Call the 2pr.io MCP tools on behalf of the signed-in user."}}}},"personalToken":{"type":"http","scheme":"bearer","description":"Personal token created by the account owner under https://2pr.io/app/preferences. Revocable there at any time."}},"schemas":{"InitializeRequest":{"type":"object","required":["jsonrpc","id","method"],"properties":{"jsonrpc":{"type":"string","const":"2.0"},"id":{"description":"JSON-RPC request id. Omit it to send a notification (answered with 202).","oneOf":[{"type":"string"},{"type":"integer"}]},"method":{"type":"string","const":"initialize"},"params":{"type":"object","properties":{"protocolVersion":{"type":"string","example":"2025-06-18"},"capabilities":{"type":"object"},"clientInfo":{"type":"object","properties":{"name":{"type":"string"},"version":{"type":"string"}}}}}}},"PingRequest":{"type":"object","required":["jsonrpc","id","method"],"properties":{"jsonrpc":{"type":"string","const":"2.0"},"id":{"description":"JSON-RPC request id. Omit it to send a notification (answered with 202).","oneOf":[{"type":"string"},{"type":"integer"}]},"method":{"type":"string","const":"ping"}}},"ToolsListRequest":{"type":"object","description":"Returns every tool with its live input schema and MCP annotations.","required":["jsonrpc","id","method"],"properties":{"jsonrpc":{"type":"string","const":"2.0"},"id":{"description":"JSON-RPC request id. Omit it to send a notification (answered with 202).","oneOf":[{"type":"string"},{"type":"integer"}]},"method":{"type":"string","const":"tools/list"}}},"ToolsCallRequest":{"type":"object","description":"Invoke one of the 43 tools. `params.name` selects the tool and fixes the shape of `params.arguments`.","required":["jsonrpc","id","method","params"],"properties":{"jsonrpc":{"type":"string","const":"2.0"},"id":{"description":"JSON-RPC request id. Omit it to send a notification (answered with 202).","oneOf":[{"type":"string"},{"type":"integer"}]},"method":{"type":"string","const":"tools/call"},"params":{"oneOf":[{"$ref":"#/components/schemas/AnalyticsGetPostingTimesParams"},{"$ref":"#/components/schemas/AnalyticsGetPostStatsParams"},{"$ref":"#/components/schemas/AnalyticsGetProfileStatsParams"},{"$ref":"#/components/schemas/CreatorAddParams"},{"$ref":"#/components/schemas/CreatorListParams"},{"$ref":"#/components/schemas/DraftAttachImagesParams"},{"$ref":"#/components/schemas/DraftAttachMediaParams"},{"$ref":"#/components/schemas/DraftCreateParams"},{"$ref":"#/components/schemas/DraftGetParams"},{"$ref":"#/components/schemas/DraftRefineParams"},{"$ref":"#/components/schemas/EngageDraftRepliesParams"},{"$ref":"#/components/schemas/EngageGetInboxParams"},{"$ref":"#/components/schemas/EngagePostReplyParams"},{"$ref":"#/components/schemas/EngageRefreshPostsParams"},{"$ref":"#/components/schemas/EngageSetReactionParams"},{"$ref":"#/components/schemas/InspirationGetTopicsParams"},{"$ref":"#/components/schemas/LinkedinFetchProfileParams"},{"$ref":"#/components/schemas/LinkedinSearchMentionsParams"},{"$ref":"#/components/schemas/MediaCommitUploadParams"},{"$ref":"#/components/schemas/MediaGenerateImageParams"},{"$ref":"#/components/schemas/MediaPrepareUploadParams"},{"$ref":"#/components/schemas/MediaUploadParams"},{"$ref":"#/components/schemas/MediaUploadFromUrlParams"},{"$ref":"#/components/schemas/OutboundAddProspectParams"},{"$ref":"#/components/schemas/OutboundListProspectsParams"},{"$ref":"#/components/schemas/OutboundRemoveProspectParams"},{"$ref":"#/components/schemas/PersonaAppendDirectiveParams"},{"$ref":"#/components/schemas/PersonaGetParams"},{"$ref":"#/components/schemas/PersonaReplaceParams"},{"$ref":"#/components/schemas/PostDeleteParams"},{"$ref":"#/components/schemas/PostListParams"},{"$ref":"#/components/schemas/PostPublishParams"},{"$ref":"#/components/schemas/PostScheduleParams"},{"$ref":"#/components/schemas/PostUpdateParams"},{"$ref":"#/components/schemas/PreferencesGetParams"},{"$ref":"#/components/schemas/PreferencesUpdateParams"},{"$ref":"#/components/schemas/ProfileListParams"},{"$ref":"#/components/schemas/ViralDismissPostParams"},{"$ref":"#/components/schemas/ViralSavePostParams"},{"$ref":"#/components/schemas/ViralSearchParams"},{"$ref":"#/components/schemas/XConnectParams"},{"$ref":"#/components/schemas/XPublishParams"},{"$ref":"#/components/schemas/XScheduleParams"}],"discriminator":{"propertyName":"name","mapping":{"analytics_getPostingTimes":"#/components/schemas/AnalyticsGetPostingTimesParams","analytics_getPostStats":"#/components/schemas/AnalyticsGetPostStatsParams","analytics_getProfileStats":"#/components/schemas/AnalyticsGetProfileStatsParams","creator_add":"#/components/schemas/CreatorAddParams","creator_list":"#/components/schemas/CreatorListParams","draft_attachImages":"#/components/schemas/DraftAttachImagesParams","draft_attachMedia":"#/components/schemas/DraftAttachMediaParams","draft_create":"#/components/schemas/DraftCreateParams","draft_get":"#/components/schemas/DraftGetParams","draft_refine":"#/components/schemas/DraftRefineParams","engage_draftReplies":"#/components/schemas/EngageDraftRepliesParams","engage_getInbox":"#/components/schemas/EngageGetInboxParams","engage_postReply":"#/components/schemas/EngagePostReplyParams","engage_refreshPosts":"#/components/schemas/EngageRefreshPostsParams","engage_setReaction":"#/components/schemas/EngageSetReactionParams","inspiration_getTopics":"#/components/schemas/InspirationGetTopicsParams","linkedin_fetchProfile":"#/components/schemas/LinkedinFetchProfileParams","linkedin_searchMentions":"#/components/schemas/LinkedinSearchMentionsParams","media_commitUpload":"#/components/schemas/MediaCommitUploadParams","media_generateImage":"#/components/schemas/MediaGenerateImageParams","media_prepareUpload":"#/components/schemas/MediaPrepareUploadParams","media_upload":"#/components/schemas/MediaUploadParams","media_uploadFromUrl":"#/components/schemas/MediaUploadFromUrlParams","outbound_addProspect":"#/components/schemas/OutboundAddProspectParams","outbound_listProspects":"#/components/schemas/OutboundListProspectsParams","outbound_removeProspect":"#/components/schemas/OutboundRemoveProspectParams","persona_appendDirective":"#/components/schemas/PersonaAppendDirectiveParams","persona_get":"#/components/schemas/PersonaGetParams","persona_replace":"#/components/schemas/PersonaReplaceParams","post_delete":"#/components/schemas/PostDeleteParams","post_list":"#/components/schemas/PostListParams","post_publish":"#/components/schemas/PostPublishParams","post_schedule":"#/components/schemas/PostScheduleParams","post_update":"#/components/schemas/PostUpdateParams","preferences_get":"#/components/schemas/PreferencesGetParams","preferences_update":"#/components/schemas/PreferencesUpdateParams","profile_list":"#/components/schemas/ProfileListParams","viral_dismissPost":"#/components/schemas/ViralDismissPostParams","viral_savePost":"#/components/schemas/ViralSavePostParams","viral_search":"#/components/schemas/ViralSearchParams","x_connect":"#/components/schemas/XConnectParams","x_publish":"#/components/schemas/XPublishParams","x_schedule":"#/components/schemas/XScheduleParams"}}}}},"AnalyticsGetPostingTimesParams":{"type":"object","description":"Posting-time history for the profile over the last 365 days — ISO timestamps of every LinkedIn post. Use to build best-time-to-post heatmaps. Requires an active Pro/Team subscription.","x-mcp-annotations":{"title":"Get Best Posting Times","readOnlyHint":true,"idempotentHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"analytics_getPostingTimes"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"}},"required":["profile_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"AnalyticsGetPostStatsParams":{"type":"object","description":"Per-post analytics for the profile's LinkedIn posts: likes, comments, post type, URL, posted timestamp, and time-series metrics snapshot. Supports pagination and sorting. Requires an active Pro/Team subscription.","x-mcp-annotations":{"title":"Get Post Analytics","readOnlyHint":true,"idempotentHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"analytics_getPostStats"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"start_date":{"type":"string","format":"date-time"},"end_date":{"type":"string","format":"date-time"},"offset":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":100},"sort_by":{"type":"string","enum":["created_at_linkedin","likes","comments"]},"sort_direction":{"type":"string","enum":["asc","desc"]}},"required":["profile_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"AnalyticsGetProfileStatsParams":{"type":"object","description":"Fetch the profile's LinkedIn analytics dashboard — current follower count, daily follower changes, daily impressions/reactions/comments, members reached total, and daily post counts. Works for personal and company profiles. Default window is the last 730 days (personal) or 365 days (company, LinkedIn cap). Requires an active Pro/Team subscription.","x-mcp-annotations":{"title":"Get Profile Analytics","readOnlyHint":true,"idempotentHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"analytics_getProfileStats"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"start_date":{"type":"string","format":"date-time","description":"ISO 8601 timestamp. Defaults to ~2 years ago."},"end_date":{"type":"string","format":"date-time","description":"ISO 8601 timestamp. Defaults to now."}},"required":["profile_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"CreatorAddParams":{"type":"object","description":"Add a LinkedIn creator (by profile URL) to the caller's saved creator list. Capped at 20 creators per list.","x-mcp-annotations":{"title":"Add Creator","readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"creator_add"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"creator_url":{"type":"string","minLength":1}},"required":["profile_id","creator_url"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"CreatorListParams":{"type":"object","description":"List the caller's saved LinkedIn creators (the \"My Creators\" list — auto-created on first call).","x-mcp-annotations":{"title":"List Saved Creators","readOnlyHint":true,"idempotentHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"creator_list"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"}},"required":["profile_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"DraftAttachImagesParams":{"type":"object","description":"Attach one or more images to a draft by their public URL so they show in the draft editor and go out with the post. Pass the 2pr-hosted URLs surfaced in the conversation context when the user attaches an image, or a URL returned by media_upload / media_uploadFromUrl. LinkedIn allows up to 9 images per post; attaching images replaces any existing video or document on the draft (a post can carry only one media kind). `alt_texts` is optional and index-aligned to `image_urls`. Returns the updated draft with its full `image_urls` list.","x-mcp-annotations":{"title":"Attach Images to Draft","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"draft_attachImages"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"draft_id":{"type":"string","format":"uuid"},"image_urls":{"type":"array","items":{"type":"string","format":"uri"},"minItems":1,"maxItems":9},"alt_texts":{"type":"array","items":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}]}}},"required":["profile_id","draft_id","image_urls"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"DraftAttachMediaParams":{"type":"object","description":"Attach a single video or PDF document to a draft by its public 2pr URL so it shows in the draft editor and goes out with the post. Use draft_attachImages for images instead. A LinkedIn post can carry only one media kind, so attaching a video or document replaces any images, video, or document already on the draft. `media_type` must match the file: `video` for mp4/mov, `document` for PDF. Pass a URL returned by media_upload / media_uploadFromUrl / media_commitUpload — their `kind` tells you which `media_type` to use. `title` is optional; for a document it becomes the LinkedIn document title. Returns the updated draft.","x-mcp-annotations":{"title":"Attach Media to Draft","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"draft_attachMedia"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"draft_id":{"type":"string","format":"uuid"},"media_type":{"type":"string","enum":["video","document"]},"url":{"type":"string","format":"uri"},"title":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}]}},"required":["profile_id","draft_id","media_type","url"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"DraftCreateParams":{"type":"object","description":"Create a new draft post from the user input text and render it as three variants of the SAME angle: `regular_body` (standard medium-form), `extended_body` (longer, more depth), and `original_body` (the raw frontier-model baseline: natural paragraphs, none of our house style layered on). The three variants are NOT different hook angles. For angle diversity (different framings of the same topic), call this tool multiple times with different `user_input_text` framings. To ground the post in a web page, article, YouTube video, LinkedIn post, or Reddit thread, pass its link as `source_url` and put the angle/instructions in `user_input_text` (e.g. \"a contrarian take on this\"); the server fetches and extracts the page and folds it into the generation. To draft FROM a file or voice note the user attached (its 2pr storage URL is given in the conversation context), pass that URL as `source_file_url`: documents (PDF/DOCX/PPTX) are read as the source and audio/video (voice notes, MP4) are transcribed, with `user_input_text` the optional instruction alongside it. To draft FROM an image the user attached (make the post ABOUT what is in the picture, not just decorate with it), pass its 2pr URL in `source_image_urls`: each image is vision-described into the generation AND automatically attached to the new draft, so it needs no separate draft_attachImages call. Provide at least one of `user_input_text`, `source_url`, `source_file_url`, or `source_image_urls`. Generation runs synchronously so the returned `url` opens an already-populated editor. Takes ~10-20s (longer with `source_url`). If generation fails the draft is still created with empty bodies and the caller can retry from the editor.","x-mcp-annotations":{"title":"Create Draft","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"draft_create"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"user_input_text":{"type":"string","minLength":1,"maxLength":10000},"source_url":{"type":"string","format":"uri","description":"Optional public http(s) link to ground the post in (article, YouTube, LinkedIn post, Reddit thread). Its extracted content is folded into the generation alongside user_input_text."},"source_file_url":{"type":"string","format":"uri"},"source_image_urls":{"type":"array","items":{"type":"string","format":"uri"},"minItems":1,"maxItems":4,"description":"2pr storage URLs of attached images to draft FROM: each is vision-described into the generation and auto-attached to the new draft. Use the hosted URLs given in the conversation context for user attachments."}},"required":["profile_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"DraftGetParams":{"type":"object","description":"Read an existing draft by id. Returns the three variants of the same angle (`regular_body` standard, `extended_body` longer, `original_body` the raw frontier-model baseline in natural paragraphs), plus `first_comment` and any attached media (images/video/document). On the chat surface the attached images are also shown to you as vision blocks alongside the result, so you can see and comment on their actual content. Use this before scheduling/publishing a draft when you need to inspect or copy its contents — or pass `draft_id` directly into post_publish/post_schedule and let them hydrate from the draft.","x-mcp-annotations":{"title":"Get Draft","readOnlyHint":true,"idempotentHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"draft_get"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"draft_id":{"type":"string","format":"uuid"}},"required":["profile_id","draft_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"DraftRefineParams":{"type":"object","description":"Replace a draft body WHOLESALE. Only the provided fields are written. Variants follow draft_create semantics: `regular_body` standard, `extended_body` longer, `original_body` the raw frontier-model baseline in natural paragraphs. Same angle, different treatment. Pass `variant` plus the MATCHING body field to rewrite a single variant and leave the other two untouched. WARNING: the body you send REPLACES what is stored, so any edit the user made in the editor since the draft was last read is overwritten. Returns the updated draft and a `url` deep link to the draft editor.","x-mcp-annotations":{"title":"Refine Draft","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"draft_refine"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"draft_id":{"type":"string","format":"uuid"},"variant":{"type":"string","enum":["regular","extended","original"]},"user_input_text":{"type":["string","null"]},"regular_body":{"type":["string","null"],"description":"To tag a person or company, paste the `mention` string from linkedin_searchMentions inline where the name should appear (`@[Name](urn:li:person:X)` or `@[Name](urn:li:organization:N)`). Never invent a URN. Tags do not work in first_comment."},"extended_body":{"type":["string","null"],"description":"To tag a person or company, paste the `mention` string from linkedin_searchMentions inline where the name should appear (`@[Name](urn:li:person:X)` or `@[Name](urn:li:organization:N)`). Never invent a URN. Tags do not work in first_comment."},"original_body":{"type":["string","null"],"description":"To tag a person or company, paste the `mention` string from linkedin_searchMentions inline where the name should appear (`@[Name](urn:li:person:X)` or `@[Name](urn:li:organization:N)`). Never invent a URN. Tags do not work in first_comment."},"first_comment":{"type":["string","null"]},"first_comment_image_url":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}]},"is_copied":{"type":"boolean"},"is_shared":{"type":"boolean"}},"required":["profile_id","draft_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"EngageDraftRepliesParams":{"type":"object","description":"Generate 5 AI reply variants for a LinkedIn comment, using the user's persona and custom engage instructions. Each reply is capped at 220 characters. Pass the comment text and (optionally) the parent post text and commenter identity for better grounding.","x-mcp-annotations":{"title":"Draft Comment Replies","readOnlyHint":true,"openWorldHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"engage_draftReplies"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"comment_text":{"type":"string","minLength":1,"maxLength":2000},"post_text":{"anyOf":[{"type":"string","maxLength":10000},{"type":"null"}]},"commenter_name":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}]},"commenter_headline":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}]}},"required":["profile_id","comment_text"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"EngageGetInboxParams":{"type":"object","description":"Fetch the user's LinkedIn engagement inbox: top-level comments on their recent posts (last 30 days, up to 20 posts), with parent post context and a `needs_reply` flag. Served from a stored copy that refreshes when the user opens the Engage page, not on every call; `freshness_note` in the result says how old the copy is, and a very recent comment may not be in it yet. Set only_needs_reply:true to filter out comments the user has already replied to.","x-mcp-annotations":{"title":"Get Engagement Inbox","readOnlyHint":true,"idempotentHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"engage_getInbox"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"only_needs_reply":{"type":"boolean"}},"required":["profile_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"EngagePostReplyParams":{"type":"object","description":"Post a reply to a LinkedIn post or comment. Omit parent_comment_urn to post a top-level comment on the share; provide it (a urn:li:comment:(...) URN) to reply within a comment thread. Returns the new comment's URN.","x-mcp-annotations":{"title":"Post Comment Reply","destructiveHint":true,"openWorldHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"engage_postReply"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"share_urn":{"type":"string","pattern":"^urn:li:"},"text":{"type":"string","minLength":1,"maxLength":1250},"parent_comment_urn":{"anyOf":[{"type":"string","pattern":"^urn:li:comment:"},{"type":"null"}]}},"required":["profile_id","share_urn","text"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"EngageRefreshPostsParams":{"type":"object","description":"Refresh the user's LinkedIn posts from Datawave (the data source the engage inbox reads). Slow — can take 2-3 minutes. Use when the inbox looks stale or after the user posts off-platform. Also marks the profile as synced so the on-load auto-refresh won't re-fire. Returns the number of posts upserted.","x-mcp-annotations":{"title":"Refresh Engagement Posts","readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"engage_refreshPosts"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"}},"required":["profile_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"EngageSetReactionParams":{"type":"object","description":"Set or clear the caller's reaction to a LinkedIn comment. Pass a reaction_type to react (LIKE, PRAISE, EMPATHY, INTEREST, APPRECIATION, ENTERTAINMENT); pass null to clear an existing reaction. Idempotent: setting the same reaction twice is a no-op.","x-mcp-annotations":{"title":"React to Comment","readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"engage_setReaction"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"comment_urn":{"type":"string","pattern":"^urn:li:comment:\\(urn:li:[a-z]+:[^,]+,\\d+\\)$"},"reaction_type":{"anyOf":[{"type":"string","enum":["LIKE","PRAISE","EMPATHY","INTEREST","APPRECIATION","ENTERTAINMENT"]},{"type":"null"}]}},"required":["profile_id","comment_urn","reaction_type"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"InspirationGetTopicsParams":{"type":"object","description":"Read the user's current inspiration feed: the personalized post ideas 2pr.io generates from their sources (their own posts and comments, saved creators, articles, Reddit, etc.). Returns each idea's title, source, and a source label. Use when the user asks for post ideas, inspiration, or \"what should I write about\". Returns an empty list when the feed has not been generated yet.","x-mcp-annotations":{"title":"Get Inspiration Topics","readOnlyHint":true,"idempotentHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"inspiration_getTopics"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"}},"required":["profile_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"LinkedinFetchProfileParams":{"type":"object","description":"Kick off background enrichment of the caller's active profile from a LinkedIn URL (pulls headline, summary, recent posts, etc.). Returns the current profile snapshot immediately — the enriched fields land asynchronously.","x-mcp-annotations":{"title":"Fetch LinkedIn Profile","readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"linkedin_fetchProfile"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"linkedin_url":{"type":"string","minLength":1}},"required":["profile_id","linkedin_url"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"LinkedinSearchMentionsParams":{"type":"object","description":"Resolve a person or company to the LinkedIn URN needed to @-mention them in a post. Query with a full name, a company name, or a LinkedIn profile or company page URL. Each result carries a ready-to-use `mention` string: paste it verbatim into the post `text` where the tag should appear and LinkedIn renders a real tag on publish. Tags only work in the post body, not in `first_comment`. Coverage is partial: a person is found only if they follow a page the user administers, have engaged with the user, or their profile URL is passed directly; a company is found only by an exact page name or URL. Needs the user's LinkedIn account connected. An empty list means no match: if the query was a name, ask the user for the profile URL once; otherwise post without the tag. Never invent a URN, an unknown one publishes as literal text.","x-mcp-annotations":{"title":"Search LinkedIn Mentions","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"linkedin_searchMentions"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"query":{"type":"string","minLength":1,"maxLength":200,"description":"A full name, a company name, or a LinkedIn profile or company page URL."},"type":{"type":"string","enum":["person","company"],"description":"Limit the search to people or companies. Omit to search both."}},"required":["profile_id","query"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"MediaCommitUploadParams":{"type":"object","description":"Step 2 of the two-step upload flow. Pass the `storage_path` returned by media_prepareUpload after your PUT upload completed. Server verifies the object exists, size is within the per-type limit, and content type is allowed, then returns the public URL and media `kind`. The returned `kind` tells you where the URL goes: attach to a draft with draft_attachImages (`image`) or draft_attachMedia (`video`/`document`), or pass it straight to post_publish/post_schedule as `image_urls` (image), `video_url` (video), or `document_url` + `document_title` (document — the `alt_text` you supply is used as the title).","x-mcp-annotations":{"title":"Commit Media Upload","readOnlyHint":false,"destructiveHint":false,"idempotentHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"media_commitUpload"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"storage_path":{"type":"string","minLength":1,"maxLength":500},"alt_text":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}]}},"required":["profile_id","storage_path"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"MediaGenerateImageParams":{"type":"object","description":"Generate an AI image from a text prompt (the same \"create image\" pipeline the draft editor uses) and return a hosted 2pr public URL. Pass that url to draft_attachImages, or in `image_urls` on post_publish/post_schedule. `prompt` should describe the image you want (the generator targets a 4:5 portrait aspect ratio suited to LinkedIn). Optional `reference_image_urls` (up to 3 public image urls, <= 10 MB each) edits, varies, or combines those images instead of generating from scratch — pass several when the user gives you several (two brand logos to put in one scene, a screenshot plus a style reference), and describe in `prompt` what to do with each. An image the user attached in chat is already hosted: use its URL here rather than describing it from memory. `backend` defaults to a fast, cheap model for fresh generations and to `google_banana_pro` (best edit quality) when reference images are set; an explicit `backend` is always honored, so pass `openai` if the user asks for the OpenAI/ChatGPT image model. Requires an active subscription; rate-limited.","x-mcp-annotations":{"title":"Generate Image","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"media_generateImage"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"prompt":{"type":"string","minLength":1,"maxLength":2000},"backend":{"type":"string","enum":["google_banana_lite","google_banana_pro","openai"]},"reference_image_urls":{"type":"array","items":{"type":"string","format":"uri"},"minItems":1,"maxItems":3,"description":"Public image URLs to edit, vary, or combine. Prefer this over reference_image_url."},"reference_image_url":{"type":"string","format":"uri","description":"Deprecated single-image form of reference_image_urls; still accepted."},"alt_text":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}]}},"required":["profile_id","prompt"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"MediaPrepareUploadParams":{"type":"object","description":"Step 1 of the two-step upload flow. Returns a signed PUT URL the agent can upload bytes to out-of-band (no base64 in context). After the PUT succeeds, call media_commitUpload with `storage_path` to finalise and get the public URL. Use this for any local file — it scales to the full per-type ceiling (10 MB images, 100 MB video/PDF), unlike media_upload which is bottlenecked by tool-call context limits. Supported content types: image/png|jpeg|webp|gif, video/mp4, video/quicktime, application/pdf.","x-mcp-annotations":{"title":"Prepare Media Upload","readOnlyHint":false,"destructiveHint":false,"idempotentHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"media_prepareUpload"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"content_type":{"type":"string","enum":["image/png","image/jpeg","image/jpg","image/webp","image/gif","video/mp4","video/quicktime","application/pdf"]}},"required":["profile_id","content_type"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"MediaUploadParams":{"type":"object","description":"Upload an image, video, or PDF document (base64-encoded) to 2pr storage and return its public URL and media `kind`. Supported content types: image/png, image/jpeg, image/webp, image/gif (<= 10 MB); video/mp4, video/quicktime (<= 100 MB); application/pdf (<= 100 MB). The returned `kind` tells you where the URL goes: attach to a draft with draft_attachImages (`image`) or draft_attachMedia (`video`/`document`), or pass it straight to post_publish/post_schedule as `image_urls` (image), `video_url` (video), or `document_url` + `document_title` (document — the `alt_text` you supply is used as the title). For video and PDF, prefer the two-step media_prepareUpload/media_commitUpload flow — files this large rarely fit inside a single tool call as base64.","x-mcp-annotations":{"title":"Upload Media","readOnlyHint":false,"destructiveHint":false},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"media_upload"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"filename":{"type":"string","minLength":1,"maxLength":200},"content_type":{"type":"string","enum":["image/png","image/jpeg","image/jpg","image/webp","image/gif","video/mp4","video/quicktime","application/pdf"]},"data_base64":{"type":"string","minLength":1,"description":"Raw base64 (no `data:` prefix). Decoded byte size must be within the per-type limit."},"alt_text":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"description":"Alt text for images; used as the document title for PDFs."}},"required":["profile_id","filename","content_type","data_base64"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"MediaUploadFromUrlParams":{"type":"object","description":"Fetch a media file from a public URL and re-upload it to 2pr storage. Returns the rehosted public URL and media `kind`. Use this when the user has the file at any public link (imgbb, Dropbox share, an existing CDN URL) and the agent cannot read the raw bytes itself. Allowed content types: image/png|jpeg|webp|gif (<= 10 MB); video/mp4, video/quicktime (<= 100 MB); application/pdf (<= 100 MB). The returned `kind` tells you where the URL goes: attach to a draft with draft_attachImages (`image`) or draft_attachMedia (`video`/`document`), or pass it straight to post_publish/post_schedule as `image_urls` (image), `video_url` (video), or `document_url` + `document_title` (document — the `alt_text` you supply is used as the title).","x-mcp-annotations":{"title":"Upload Media from URL","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"media_uploadFromUrl"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"url":{"type":"string","format":"uri"},"filename":{"type":"string","minLength":1,"maxLength":200},"alt_text":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}]}},"required":["profile_id","url"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"OutboundAddProspectParams":{"type":"object","description":"Add a person to the \"Engage with\" prospect list (Outbound comments) by LinkedIn member profile URL or bare vanity handle. A plain name only works when it matches exactly one creator we already index, so prefer the profile URL. Company pages are rejected. Adding someone already on the list returns the existing entry. The list has a fixed number of slots; outbound_listProspects reports how many are left.","x-mcp-annotations":{"title":"Add Prospect","readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"outbound_addProspect"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"linkedin_url":{"type":"string","minLength":1,"maxLength":500}},"required":["profile_id","linkedin_url"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"OutboundListProspectsParams":{"type":"object","description":"List the \"Engage with\" prospect list on the Engage page (Outbound comments): the people whose posts the user tracks to comment on. Separate from the \"My Creators\" list (creator_list), which feeds the viral library. Returns the prospects plus how many slots the list has and how many are still free.","x-mcp-annotations":{"title":"List Prospects","readOnlyHint":true,"idempotentHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"outbound_listProspects"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"}},"required":["profile_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"OutboundRemoveProspectParams":{"type":"object","description":"Remove a prospect from the \"Engage with\" list (Outbound comments) by the id outbound_listProspects returns. Frees a slot; their posts stop appearing in the outbound feed.","x-mcp-annotations":{"title":"Remove Prospect","destructiveHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"outbound_removeProspect"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"prospect_id":{"type":"string","format":"uuid"}},"required":["profile_id","prospect_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"PersonaAppendDirectiveParams":{"type":"object","description":"Append a voice/style directive (single bullet, at most 1000 characters) into the user-directives section of a profile's persona. Creates a baseline persona from onboarding if none exists. For anything longer, split it across several calls or use persona_replace.","x-mcp-annotations":{"title":"Add Voice Directive","readOnlyHint":false,"destructiveHint":false,"idempotentHint":false},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"persona_appendDirective"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"directive":{"type":"string","minLength":1,"maxLength":1000}},"required":["profile_id","directive"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"PersonaGetParams":{"type":"object","description":"Get the persona / voice brief for a profile in markdown. Returns null content if no persona is set and no onboarding data exists to derive one.","x-mcp-annotations":{"title":"Get Voice Profile","readOnlyHint":true,"idempotentHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"persona_get"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"}},"required":["profile_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"PersonaReplaceParams":{"type":"object","description":"Replace the full persona / voice brief content for a profile. Normalizes via cleanPersonaForEditor and writes a version snapshot.","x-mcp-annotations":{"title":"Replace Voice Profile","destructiveHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"persona_replace"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"content":{"type":"string","minLength":1}},"required":["profile_id","content"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"PostDeleteParams":{"type":"object","description":"Cancel a scheduled post that hasn't been published yet — removes a queued post from the planner. This does NOT and cannot delete or remove a post that is already live on LinkedIn. Throws 404 if the post is already posted or does not exist.","x-mcp-annotations":{"title":"Delete Scheduled Post","destructiveHint":true,"idempotentHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"post_delete"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"scheduled_post_id":{"type":"string","format":"uuid"}},"required":["profile_id","scheduled_post_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"PostListParams":{"type":"object","description":"List LinkedIn posts for a profile from the planner queue. Defaults to pending rows only (`status=scheduled`, soonest first); pass `posted`, `failed`, or `all` for history (most recent first). Use this to find a `scheduled_post_id` before calling post_update or post_delete. Filter with `draftpost_id` when the user has a draft open and you need its queued slot. `text_preview` is TRUNCATED for display only. Never pass it back as post_update `text`, or you will overwrite the post with a clipped copy.","x-mcp-annotations":{"title":"List Scheduled Posts","readOnlyHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"post_list"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"draftpost_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["scheduled","posted","failed","all"]}},"required":["profile_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"PostPublishParams":{"type":"object","description":"Publish a post to LinkedIn immediately. Persists a row in scheduled_posts with status=posted on success. Pass `draftpost_id` to hydrate text/image_urls/first_comment/video/document from the draft (defaults to the regular body — override with `body_variant`). Explicit fields take precedence over draft fields. `text` is optional when media is attached: a media-only call publishes with no caption. Set do_not_share:true to record the post as published without actually calling LinkedIn (used for tracking off-platform sharing). `first_comment_image_url` attaches an image (or GIF) to the first comment.","x-mcp-annotations":{"title":"Publish Post","destructiveHint":true,"openWorldHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"post_publish"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"text":{"type":"string","minLength":1,"description":"To tag a person or company, paste the `mention` string from linkedin_searchMentions inline where the name should appear (`@[Name](urn:li:person:X)` or `@[Name](urn:li:organization:N)`). Never invent a URN. Tags do not work in first_comment."},"image_urls":{"type":"array","items":{"type":"string","format":"uri"},"maxItems":9},"author_urn":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]},"first_comment":{"anyOf":[{"type":"string","maxLength":1250},{"type":"null"}]},"first_comment_image_url":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}]},"draftpost_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"body_variant":{"type":"string","enum":["regular","extended","original"]},"do_not_share":{"type":"boolean"},"document_url":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}]},"document_title":{"anyOf":[{"type":"string","minLength":1,"maxLength":100},{"type":"null"}]},"video_url":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}]}},"required":["profile_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"PostScheduleParams":{"type":"object","description":"Schedule a post for future delivery to LinkedIn. Pass `draftpost_id` to hydrate text/image_urls/first_comment/video/document from the draft (defaults to the regular body — override with `body_variant`). Explicit fields take precedence. `text` is optional when media is attached: a media-only call schedules with no caption. The scheduled-posts cron picks it up at scheduled_time. `first_comment_image_url` attaches an image (or GIF) to the first comment. Returns a `url` deep link to the planner, where the user reviews and edits scheduled posts.","x-mcp-annotations":{"title":"Schedule Post","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"post_schedule"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"text":{"type":"string","minLength":1,"description":"To tag a person or company, paste the `mention` string from linkedin_searchMentions inline where the name should appear (`@[Name](urn:li:person:X)` or `@[Name](urn:li:organization:N)`). Never invent a URN. Tags do not work in first_comment."},"image_urls":{"type":"array","items":{"type":"string","format":"uri"},"maxItems":9},"author_urn":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]},"first_comment":{"anyOf":[{"type":"string","maxLength":1250},{"type":"null"}]},"first_comment_image_url":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}]},"draftpost_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"body_variant":{"type":"string","enum":["regular","extended","original"]},"do_not_share":{"type":"boolean"},"document_url":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}]},"document_title":{"anyOf":[{"type":"string","minLength":1,"maxLength":100},{"type":"null"}]},"video_url":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}]},"scheduled_time":{"type":"string","format":"date-time"}},"required":["profile_id","scheduled_time"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"PostUpdateParams":{"type":"object","description":"Update an existing scheduled LinkedIn post without cancelling and re-booking it. Pass `scheduled_post_id` from post_list (or from a prior post_schedule result). Provide only the fields that change: `text`, `image_urls`, `scheduled_time`, `first_comment`, or video/document fields. Pass `draftpost_id` to pull the draft's current media onto the queued post (use this after draft_attachImages); it does NOT touch the queued text, which the user may have edited in the planner. To change copy, pass `text` explicitly. Use this when the user asks to add a photo, edit copy, or move the time on a post that is already in the planner. Does not work on posts that have already gone live. Returns the same planner deep link as post_schedule.","x-mcp-annotations":{"title":"Update Scheduled Post","destructiveHint":true,"openWorldHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"post_update"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"text":{"type":"string","minLength":1,"description":"To tag a person or company, paste the `mention` string from linkedin_searchMentions inline where the name should appear (`@[Name](urn:li:person:X)` or `@[Name](urn:li:organization:N)`). Never invent a URN. Tags do not work in first_comment."},"image_urls":{"type":"array","items":{"type":"string","format":"uri"},"maxItems":9},"author_urn":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]},"first_comment":{"anyOf":[{"type":"string","maxLength":1250},{"type":"null"}]},"first_comment_image_url":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}]},"draftpost_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"do_not_share":{"type":"boolean"},"document_url":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}]},"document_title":{"anyOf":[{"type":"string","minLength":1,"maxLength":100},{"type":"null"}]},"video_url":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}]},"scheduled_post_id":{"type":"string","format":"uuid"},"scheduled_time":{"type":"string","format":"date-time"}},"required":["profile_id","scheduled_post_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"PreferencesGetParams":{"type":"object","description":"Read the content preferences for a profile: language, theme, audience, goal, persona, tone, reach-vs-authenticity priority, geography, emoji use, Engage reply instructions, and which inspiration sources, subreddits and feeds are in play. Use this before changing anything so you say what is actually set. Not the voice brief, which is persona_get.","x-mcp-annotations":{"title":"Get Preferences","readOnlyHint":true,"idempotentHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"preferences_get"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"}},"required":["profile_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"PreferencesUpdateParams":{"type":"object","description":"Change content preferences for a profile. Send only the fields to change; pass null to clear one. It takes effect everywhere at once (drafting, the inspiration feed, Engage replies). Cannot set the voice brief (use persona_replace / persona_appendDirective) or any internal flag.","x-mcp-annotations":{"title":"Update Preferences","readOnlyHint":false,"destructiveHint":false,"idempotentHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"preferences_update"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"language":{"type":["string","null"],"description":"Language the posts are written in, as an English language name (\"English\", \"Russian\")."},"content_primary_theme":{"type":["string","null"],"description":"Main subject the creator posts about. Changing it re-derives which subreddits we track."},"target_audience":{"type":["string","null"],"description":"Who the posts are written for."},"content_goal":{"type":["string","null"],"description":"What the creator wants the posts to achieve."},"content_persona":{"type":["string","null"],"description":"The role the creator writes as (founder, engineer, coach)."},"content_tone":{"type":["string","null"],"description":"How the posts should sound (casual, direct, warm)."},"content_priority":{"type":["string","null"],"description":"Balance between reach and authenticity. Canonical values: \"Performance only\", \"Metrics first\", \"Balance\", \"Authenticity first\", \"Authenticity only\"."},"geographic_focus":{"type":["string","null"],"description":"Region the audience sits in."},"emoji_preference":{"type":["string","null"],"description":"How much emoji the posts should use."},"engage_custom_instructions":{"type":["string","null"],"description":"Standing instructions for the replies drafted in the Engage inbox."},"custom_subreddits":{"type":["string","null"],"description":"Comma-separated subreddit names to pull inspiration from, without the \"r/\" prefix. Empty falls back to theme-derived subreddits."},"inspiration_feeds":{"type":["string","null"],"description":"Comma-separated newsletter / blog feed URLs used by the inspiration feed."},"inspiration_recency_days":{"anyOf":[{"type":"integer","minimum":3,"maximum":30},{"type":"null"}],"description":"How far back the inspiration sources may look, in days."},"muted_inspiration_sources":{"type":["string","null"],"description":"Comma-separated inspiration sources to switch off. Keys: linkedin_library, x, reddit, hackernews, github, substack, my_comments."}},"required":["profile_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"ProfileListParams":{"type":"object","description":"List all profiles the caller has access to. Call this first to discover available profile_id values — most other tools require a profile_id. Also reports whether X is connected for each profile, so you can tell before attempting to publish there.","x-mcp-annotations":{"title":"List Profiles","readOnlyHint":true,"idempotentHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"profile_list"},"arguments":{"type":"object","properties":{},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"ViralDismissPostParams":{"type":"object","description":"Remove a previously saved viral post from the caller's saved-posts collection.","x-mcp-annotations":{"title":"Dismiss Viral Post","readOnlyHint":false,"destructiveHint":false,"idempotentHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"viral_dismissPost"},"arguments":{"type":"object","properties":{"viral_post_id":{"type":"string","pattern":"^\\d+$"}},"required":["viral_post_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"ViralSavePostParams":{"type":"object","description":"Save a viral post to the caller's saved-posts collection with optional tags and notes.","x-mcp-annotations":{"title":"Save Viral Post","readOnlyHint":false,"destructiveHint":false,"idempotentHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"viral_savePost"},"arguments":{"type":"object","properties":{"viral_post_id":{"type":"string","pattern":"^\\d+$"},"tags":{"type":"array","items":{"type":"string"}},"notes":{"type":"string"}},"required":["viral_post_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"ViralSearchParams":{"type":"object","description":"Search the viral LinkedIn post catalog by keyword, with optional filters (language, creator URLs, engagement thresholds, date range). Defaults to the last 12 months and reference-quality posts. Put the subject in search_query; every extra filter narrows the catalog, and results are already ranked by relevance weighted by engagement. Phrase search_query as the subject (\"warm outreach\"), not as the request (\"show me posts about warm outreach\"): every word has to appear in the post. The search broadens itself when a query is too narrow, so a thin result means the shorter query was tried too.","x-mcp-annotations":{"title":"Search Viral Posts","readOnlyHint":true,"idempotentHint":true,"openWorldHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"viral_search"},"arguments":{"type":"object","properties":{"search_query":{"type":"string","minLength":1},"geographic_focus":{"type":"string"},"languages":{"type":"string"},"user_url":{"type":"string"},"user_urls":{"type":"array","items":{"type":"string"}},"date_posted_start":{"type":"string"},"date_posted_end":{"type":"string"},"min_likes":{"type":"integer","minimum":0,"description":"Only when the user named a floor (\"at least 1000 likes\"). Omit otherwise."},"max_likes":{"type":"integer","minimum":0,"description":"Only when the user named a ceiling. Omit otherwise; never send 0."},"min_comments":{"type":"integer","minimum":0},"max_comments":{"type":"integer","minimum":0,"description":"Only when the user named a ceiling. Omit otherwise; never send 0."},"min_followers":{"type":"integer","minimum":0},"max_followers":{"type":"integer","minimum":0,"description":"Only when the user named a ceiling. Omit otherwise; never send 0."},"min_posts":{"type":"integer","minimum":0},"max_posts":{"type":"integer","minimum":0,"description":"Only when the user named a ceiling. Omit otherwise; never send 0."},"min_articles":{"type":"integer","minimum":0},"max_articles":{"type":"integer","minimum":0,"description":"Only when the user named a ceiling. Omit otherwise; never send 0."},"has_images":{"type":"boolean","description":"Set true to require an image. Omit for any post; false is ignored."},"has_videos":{"type":"boolean","description":"Set true to require a video. Omit for any post; false is ignored."},"has_carousel":{"type":"boolean","description":"Set true to require a carousel. Omit for any post; false is ignored."},"account_type":{"type":"string","enum":["person","company"]},"sort_by":{"type":"string","enum":["likes","date","relevance"]},"limit":{"type":"integer","minimum":1,"maximum":50},"offset":{"type":"integer","minimum":0}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"XConnectParams":{"type":"object","description":"ONLY for X (Twitter), and ONLY when the user explicitly asked about X or Twitter. Connecting LinkedIn is a separate flow and this tool has no effect on it. Get a one-tap URL the user opens to connect (or reconnect) their X account to a 2pr profile. There is no X settings screen yet, so this URL is the only way to authorize. Give the user the link and ask them to open it; the page tells them when it worked. The link is single-use and valid for 7 days, so it still works if they come back to it later. Also reports whether X is already connected.","x-mcp-annotations":{"title":"Connect X Account","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"x_connect"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"}},"required":["profile_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"XPublishParams":{"type":"object","description":"ONLY for X (Twitter), and ONLY when the user explicitly asked for X or Twitter. LinkedIn is the default destination for every post: if the user did not name X, use `post_publish` instead. Publishing to the wrong network cannot be undone. Publish to X immediately. Pass `text` for a single post or `thread` as an ordered array of segments for a thread. Links count as 23 characters regardless of their real length. The per-post limit depends on the connected account: 280 characters on a free account, up to 25000 on X Premium. Do NOT pre-split long content into a thread assuming 280: send it as one `text`, and only split if the tool rejects it for length. A thread of N segments costs N posts where one long post costs 1. Up to 4 images via `image_urls`, attached to the first segment of a thread. Pass `draftpost_id` to hydrate the body from a draft; a draft that is too long is an error, never a silent trim. X has no document post type, no first comment and no company pages, so PDFs, carousels and video are rejected. Requires X to be connected: call `x_connect` first if it isn't. Set do_not_share:true to record the post without actually calling X. Scheduled X posts do not appear in the LinkedIn planner; use `x_schedule` and `post_delete` to manage them.","x-mcp-annotations":{"title":"Publish to X","destructiveHint":true,"openWorldHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"x_publish"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"text":{"type":"string","minLength":1},"thread":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"maxItems":25},"image_urls":{"type":"array","items":{"type":"string","format":"uri"},"maxItems":4},"draftpost_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"body_variant":{"type":"string","enum":["regular","extended","original"]},"do_not_share":{"type":"boolean"}},"required":["profile_id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"XScheduleParams":{"type":"object","description":"ONLY for X (Twitter), and ONLY when the user explicitly asked for X or Twitter. LinkedIn is the default destination for every post: if the user did not name X, use `post_schedule` instead. Schedule a post or thread for future delivery to X. Same content rules as `x_publish`: `text` (280 characters on a free account, up to 25000 on X Premium, so do not pre-split) or `thread` as an array of segments, up to 4 images, no documents or video. The every-minute publish cron delivers it at `scheduled_time`. Scheduled X posts are NOT visible in the 2pr planner (that view is LinkedIn-only for now) — the returned `id` is what you pass to `post_delete` to cancel it before it goes out.","x-mcp-annotations":{"title":"Schedule to X","destructiveHint":true,"openWorldHint":true},"required":["name","arguments"],"properties":{"name":{"type":"string","const":"x_schedule"},"arguments":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid"},"text":{"type":"string","minLength":1},"thread":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"maxItems":25},"image_urls":{"type":"array","items":{"type":"string","format":"uri"},"maxItems":4},"draftpost_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"body_variant":{"type":"string","enum":["regular","extended","original"]},"do_not_share":{"type":"boolean"},"scheduled_time":{"type":"string","format":"date-time"}},"required":["profile_id","scheduled_time"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}},"JsonRpcResponse":{"type":"object","required":["jsonrpc","id"],"properties":{"jsonrpc":{"type":"string","const":"2.0"},"id":{"description":"JSON-RPC request id. Omit it to send a notification (answered with 202).","oneOf":[{"type":"string"},{"type":"integer"}]},"result":{"type":"object","description":"For `tools/call`: `{ content: [{ type: \"text\", text: <JSON string> }], isError: boolean }`. The text is the tool output serialized as JSON."},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"integer"},"message":{"type":"string"},"data":{}}}}},"TokenRequest":{"type":"object","required":["grant_type","client_id"],"properties":{"grant_type":{"type":"string","enum":["authorization_code","refresh_token"]},"client_id":{"type":"string"},"code":{"type":"string","description":"authorization_code grant."},"code_verifier":{"type":"string","description":"authorization_code grant (PKCE)."},"redirect_uri":{"type":"string","format":"uri","description":"authorization_code grant."},"refresh_token":{"type":"string","description":"refresh_token grant."},"resource":{"type":"string","format":"uri","example":"https://2pr.io/api/mcp"}}},"ApiError":{"type":"object","required":["detail"],"properties":{"detail":{"type":"string"}}},"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"error_description":{"type":"string"}}}}}}